Spaces:
Configuration error
Configuration error
personalbotai
Deploy Archon Dataset Sync v2.1 with branch support\n\n- Add sync_dataset.sh with DATASET_BRANCH support\n- Add Flask monitoring dashboard (app.py)\n- Add Dockerfile for HF Space deployment\n- Add comprehensive documentation\n- Security hardening (upstream protection)\n- Auto-retry with exponential backoff\n- Health checks and graceful shutdown\n\nArchon Standard: Build for Eternity
9de9a1b | #!/usr/bin/env python3 | |
| """ | |
| Archon Monitoring Dashboard for sync_dataset.sh | |
| Flask UI untuk memantau status sync dan logs | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import subprocess | |
| from datetime import datetime | |
| from pathlib import Path | |
| from flask import Flask, render_template, jsonify, request | |
| import psutil | |
| app = Flask(__name__) | |
| # Config paths | |
| PICOCLAW_HOME = os.getenv('PICOCLAW_HOME', os.path.expanduser('~/.picoclaw')) | |
| LOG_FILE = Path(PICOCLAW_HOME) / 'sync.log' | |
| STATE_FILE = Path(PICOCLAW_HOME) / 'sync.state' | |
| CONFIG_FILE = Path(PICOCLAW_HOME) / 'config.json' | |
| def read_config(): | |
| """Load configuration dari config.json atau environment""" | |
| config = { | |
| 'DATASET_REPO': os.getenv('DATASET_REPO', 'https://github.com/personalbotai/picoclaw-memory.git'), | |
| 'DATASET_BRANCH': os.getenv('DATASET_BRANCH', 'main'), | |
| 'SYNC_INTERVAL': int(os.getenv('SYNC_INTERVAL', '300')), | |
| 'PICOCLAW_HOME': PICOCLAW_HOME | |
| } | |
| if CONFIG_FILE.exists(): | |
| try: | |
| with open(CONFIG_FILE) as f: | |
| file_config = json.load(f) | |
| config.update(file_config) | |
| except Exception as e: | |
| app.logger.error(f"Failed to read config: {e}") | |
| return config | |
| def get_sync_status(): | |
| """Check if sync daemon is running""" | |
| if STATE_FILE.exists(): | |
| try: | |
| with open(STATE_FILE) as f: | |
| pid = int(f.read().strip()) | |
| if psutil.pid_exists(pid): | |
| proc = psutil.Process(pid) | |
| return { | |
| 'running': True, | |
| 'pid': pid, | |
| 'cpu_percent': proc.cpu_percent(interval=0.1), | |
| 'memory_mb': proc.memory_info().rss / 1024 / 1024, | |
| 'cmdline': proc.cmdline() | |
| } | |
| except Exception as e: | |
| app.logger.error(f"Error checking PID: {e}") | |
| return {'running': False} | |
| def get_recent_logs(lines=100): | |
| """Read recent log entries""" | |
| if not LOG_FILE.exists(): | |
| return [] | |
| try: | |
| with open(LOG_FILE) as f: | |
| all_lines = f.readlines() | |
| return all_lines[-lines:] | |
| except Exception as e: | |
| app.logger.error(f"Error reading logs: {e}") | |
| return [] | |
| def get_disk_usage(): | |
| """Get disk usage statistics""" | |
| try: | |
| stat = os.statvfs(PICOCLAW_HOME) | |
| total = stat.f_frsize * stat.f_blocks | |
| free = stat.f_frsize * stat.f_bavail | |
| used = total - free | |
| return { | |
| 'total_gb': round(total / 1024 / 1024 / 1024, 2), | |
| 'used_gb': round(used / 1024 / 1024 / 1024, 2), | |
| 'free_gb': round(free / 1024 / 1024 / 1024, 2), | |
| 'percent_used': round((used / total) * 100, 1) | |
| } | |
| except Exception as e: | |
| app.logger.error(f"Error getting disk usage: {e}") | |
| return {} | |
| def get_git_status(): | |
| """Check git status of backup directory""" | |
| backup_dir = Path(PICOCLAW_HOME) / 'backup' | |
| if not (backup_dir / '.git').exists(): | |
| return {'error': 'Not a git repository'} | |
| try: | |
| os.chdir(backup_dir) | |
| branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip() | |
| remote = subprocess.check_output(['git', 'remote', 'get-url', 'origin'], text=True).strip() | |
| status = subprocess.check_output(['git', 'status', '--porcelain'], text=True).strip() | |
| return { | |
| 'branch': branch, | |
| 'remote': remote, | |
| 'has_changes': bool(status), | |
| 'changes_count': len(status.split('\n')) if status else 0 | |
| } | |
| except subprocess.CalledProcessError as e: | |
| return {'error': str(e)} | |
| except Exception as e: | |
| return {'error': str(e)} | |
| def index(): | |
| """Main dashboard""" | |
| config = read_config() | |
| status = get_sync_status() | |
| logs = get_recent_logs(50) | |
| disk = get_disk_usage() | |
| git = get_git_status() | |
| return render_template('index.html', | |
| config=config, | |
| status=status, | |
| logs=logs, | |
| disk=disk, | |
| git=git, | |
| now=datetime.now().strftime('%Y-%m-%d %H:%M:%S') | |
| ) | |
| def api_status(): | |
| """JSON API for status""" | |
| return jsonify({ | |
| 'sync': get_sync_status(), | |
| 'disk': get_disk_usage(), | |
| 'git': get_git_status(), | |
| 'timestamp': datetime.now().isoformat() | |
| }) | |
| def api_logs(): | |
| """Get logs as JSON""" | |
| lines = request.args.get('lines', '100', type=int) | |
| logs = get_recent_logs(lines) | |
| return jsonify({'logs': logs}) | |
| def api_restart(): | |
| """Restart sync daemon (placeholder)""" | |
| # In production, this would send signal to daemon | |
| return jsonify({'status': 'ok', 'message': 'Restart signal sent'}) | |
| if __name__ == '__main__': | |
| # Create templates directory if not exists | |
| templates_dir = Path(__file__).parent / 'templates' | |
| templates_dir.mkdir(exist_ok=True) | |
| # Create index.html if not exists | |
| index_html = templates_dir / 'index.html' | |
| if not index_html.exists(): | |
| # Generate default template | |
| html_content = '''<!DOCTYPE html> | |
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Archon Sync Dashboard</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a1a; color: #e0e0e0; line-height: 1.6; } | |
| .container { max-width: 1200px; margin: 0 auto; padding: 20px; } | |
| header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; border-bottom: 2px solid #333; margin-bottom: 30px; } | |
| h1 { color: #00d4ff; font-size: 1.8em; } | |
| .status-badge { padding: 8px 16px; border-radius: 20px; font-weight: bold; } | |
| .status-running { background: #00c853; color: #fff; } | |
| .status-stopped { background: #ff4444; color: #fff; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } | |
| .card { background: #2a2a2a; border-radius: 12px; padding: 20px; border: 1px solid #333; } | |
| .card h2 { color: #00d4ff; margin-bottom: 15px; font-size: 1.2em; border-bottom: 1px solid #444; padding-bottom: 10px; } | |
| .metric { display: flex; justify-content: space-between; margin: 10px 0; } | |
| .metric-label { color: #aaa; } | |
| .metric-value { font-weight: bold; color: #fff; } | |
| .log-container { background: #0d0d0d; border-radius: 8px; padding: 15px; height: 500px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 0.9em; } | |
| .log-line { margin: 2px 0; padding: 2px 0; border-bottom: 1px solid #222; } | |
| .log-INFO { color: #4fc3f7; } | |
| .log-WARN { color: #ffb300; } | |
| .log-ERROR { color: #ff5252; } | |
| .log-DEBUG { color: #9e9e9e; } | |
| .btn { background: #00d4ff; color: #000; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: bold; } | |
| .btn:hover { background: #00b8e6; } | |
| .refresh { margin-top: 20px; text-align: center; } | |
| @media (max-width: 768px) { .container { padding: 10px; } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>๐๏ธ Archon Sync Dashboard</h1> | |
| <span class="status-badge {{ 'status-running' if status.running else 'status-stopped' }}"> | |
| {{ 'RUNNING' if status.running else 'STOPPED' }} | |
| </span> | |
| </header> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h2>๐ Configuration</h2> | |
| <div class="metric"> | |
| <span class="metric-label">Repository:</span> | |
| <span class="metric-value">{{ config.DATASET_REPO }}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Branch:</span> | |
| <span class="metric-value">{{ config.DATASET_BRANCH }}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Sync Interval:</span> | |
| <span class="metric-value">{{ config.SYNC_INTERVAL }}s</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Home Dir:</span> | |
| <span class="metric-value">{{ config.PICOCLAW_HOME }}</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>๐พ Disk Usage</h2> | |
| {% if disk %} | |
| <div class="metric"> | |
| <span class="metric-label">Total:</span> | |
| <span class="metric-value">{{ disk.total_gb }} GB</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Used:</span> | |
| <span class="metric-value">{{ disk.used_gb }} GB ({{ disk.percent_used }}%)</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Free:</span> | |
| <span class="metric-value">{{ disk.free_gb }} GB</span> | |
| </div> | |
| {% else %} | |
| <p>โ ๏ธ Unable to get disk info</p> | |
| {% endif %} | |
| </div> | |
| <div class="card"> | |
| <h2>๐ Git Status</h2> | |
| {% if git.error %} | |
| <p>โ ๏ธ {{ git.error }}</p> | |
| {% else %} | |
| <div class="metric"> | |
| <span class="metric-label">Branch:</span> | |
| <span class="metric-value">{{ git.branch }}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Remote:</span> | |
| <span class="metric-value">{{ git.remote[:50] + '...' if git.remote|length > 50 else git.remote }}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Changes:</span> | |
| <span class="metric-value">{{ git.changes_count }} file(s)</span> | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% if status.running %} | |
| <div class="card"> | |
| <h2>โ๏ธ Process Info</h2> | |
| <div class="metric"> | |
| <span class="metric-label">PID:</span> | |
| <span class="metric-value">{{ status.pid }}</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">CPU:</span> | |
| <span class="metric-value">{{ "%.1f"|format(status.cpu_percent) }}%</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">Memory:</span> | |
| <span class="metric-value">{{ "%.1f"|format(status.memory_mb) }} MB</span> | |
| </div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <div class="card"> | |
| <h2>๐ Recent Logs</h2> | |
| <div class="log-container"> | |
| {% for log in logs %} | |
| <div class="log-line log-{{ log.split(']')[1].split(']')[0] if ']' in log and ']' in log.split(']')[1] else 'DEBUG' }}"> | |
| {{ log.rstrip() }} | |
| </div> | |
| {% endfor %} | |
| </div> | |
| <div class="refresh"> | |
| <button class="btn" onclick="location.reload()">Refresh Now</button> | |
| <small style="color: #666; margin-left: 10px;">Last update: {{ now }}</small> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Auto-refresh every 30 seconds | |
| setTimeout(() => location.reload(), 30000); | |
| </script> | |
| </body> | |
| </html>''' | |
| index_html.write_text(html_content) | |
| app.run(host='0.0.0.0', port=7860, debug=False) |