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)}
@app.route('/')
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')
)
@app.route('/api/status')
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()
})
@app.route('/api/logs')
def api_logs():
"""Get logs as JSON"""
lines = request.args.get('lines', '100', type=int)
logs = get_recent_logs(lines)
return jsonify({'logs': logs})
@app.route('/api/restart', methods=['POST'])
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)