diff --git a/scripts/dashboard.py b/scripts/dashboard.py index 8bf6b78..9998df4 100644 --- a/scripts/dashboard.py +++ b/scripts/dashboard.py @@ -4,7 +4,7 @@ Momentry Dashboard — Flask web app Reads pipeline status + Redis + system health on demand """ -import json, os, subprocess, sys +import json, os, subprocess, sys, platform from pathlib import Path from flask import Flask, jsonify, render_template_string @@ -12,6 +12,12 @@ app = Flask(__name__) PROJECT = Path(__file__).resolve().parent.parent +# System role detection +HOSTNAME = platform.node() +IS_M5 = "MacBook" in HOSTNAME or "M5" in HOSTNAME +SYSTEM_ROLE = "M5 (MacBook Pro)" if IS_M5 else "M4 (Mac Mini)" +SYSTEM_COLOR = "#58a6ff" if IS_M5 else "#f0883e" + def run_status_json(): """Run pipeline_status.py and return parsed JSON""" @@ -71,7 +77,7 @@ def run_redis_info(): def run_db_info(): - """Fetch DB metrics""" + """Fetch DB metrics + current processing file""" psql = "/Users/accusys/pgsql/18.3/bin/psql" cmd = [psql, "-U", "accusys", "-d", "momentry", "-t", "-A"] result = {} @@ -91,6 +97,48 @@ def run_db_info(): result[parts[0].strip()] = int(parts[1]) except: pass + + # 所有檔案的 pipeline 進度(依檔案名去重,取最新) + try: + r = subprocess.run(cmd + ["-c", """ + SELECT DISTINCT ON (v.file_name) + v.file_uuid, v.file_name, v.status, + COALESCE(v.processing_status::text, '{}') as pstatus, + m.status as job_status + FROM dev.videos v + LEFT JOIN dev.monitor_jobs m ON m.uuid = v.file_uuid + WHERE v.status IN ('completed', 'processing') + OR m.status IS NOT NULL + ORDER BY v.file_name, GREATEST( + COALESCE(v.registration_time::timestamp, '1970-01-01'), + COALESCE(m.updated_at, '1970-01-01') + ) DESC + LIMIT 20 + """], capture_output=True, text=True, timeout=10) + seen_names = set() + files = [] + for line in r.stdout.strip().split("\n"): + if not line.strip() or "|" not in line: + continue + parts = line.split("|", 4) + if len(parts) < 5: + continue + name = parts[1].strip() + if name in seen_names: + continue + seen_names.add(name) + f = {"uuid": parts[0].strip(), "name": name, + "status": parts[2].strip(), "job_status": parts[4].strip()} + try: + ps = json.loads(parts[3]) if parts[3] and parts[3] != '{}' else {} + f["progress"] = ps.get("progress", {}) + except: + f["progress"] = {} + files.append(f) + result["files"] = files + except Exception as e: + result["files_error"] = str(e) + return result @@ -117,6 +165,7 @@ def api_db(): @app.route("/api/all") def api_all(): return jsonify({ + "system": {"hostname": HOSTNAME, "role": SYSTEM_ROLE, "is_m5": IS_M5}, "status": run_status_json(), "redis": run_redis_info(), "db": run_db_info(), @@ -162,11 +211,12 @@ button:hover { background: #2ea043; }
-
-

Momentry Dashboard

-
+
+

Momentry Dashboard 🤖 {{ SYSTEM_ROLE }}

+
- + +
@@ -189,6 +239,11 @@ button:hover { background: #2ea043; }
+
+

📁 Pipeline Progress

+
Loading...
+
+
@@ -216,24 +271,28 @@ async function fetchAll() { document.getElementById('lastUpdated').textContent = '🔄 ' + ts; try { - const status = await (await fetch('/api/status')).json(); + const all = await (await fetch('/api/all')).json(); + _lastData = all; + const status = all.status; renderChecklist(status.job); renderHealth(status.health); renderTiming(status.health?.processors); + if (all.redis) renderRedis(all.redis); + if (all.db) { renderDb(all.db); renderFileProgress(all.db); } document.getElementById('lastUpdated').textContent = '✅ ' + ts; } catch(e) { document.getElementById('checklist').innerHTML = 'Error: ' + e.message + ''; + // Fallback: try separate endpoints + try { + const s = await (await fetch('/api/status')).json(); renderChecklist(s.job); renderHealth(s.health); renderTiming(s.health?.processors); + } catch(e2) {} + try { + const r = await (await fetch('/api/redis')).json(); renderRedis(r); + } catch(e2) {} + try { + const d = await (await fetch('/api/db')).json(); renderDb(d); renderFileProgress(d); + } catch(e2) {} } - - try { - const redis = await (await fetch('/api/redis')).json(); - renderRedis(redis); - } catch(e) {} - - try { - const db = await (await fetch('/api/db')).json(); - renderDb(db); - } catch(e) {} } function renderChecklist(job) { @@ -304,6 +363,38 @@ function renderRedis(r) { document.getElementById('redis').innerHTML = h; } +const PIPELINE_STAGES = ['cut','scene','asr','asrx','yolo','ocr','face','pose','visual_chunk','story']; + +function renderFileProgress(d) { + const el = document.getElementById('fileProgress'); + if (!d || !d.files || d.files.length === 0) { + el.innerHTML = '
No files found
'; + return; + } + let h = ''; + for (const s of PIPELINE_STAGES) h += ''; + h += ''; + for (const f of d.files) { + const name = f.name.length > 50 ? f.name.slice(0,50) + '...' : f.name; + const statusIcon = f.job_status === 'running' ? '▶️' : f.job_status === 'pending' ? '⏳' : f.status === 'completed' ? '✅' : '❌'; + const progress = f.progress || {}; + h += '' + + ''; + for (const s of PIPELINE_STAGES) { + const ps = progress[s.toUpperCase()] || {}; + const st = ps.status || ''; + let icon = '⬜'; + if (st === 'completed') icon = '✅'; + else if (st === 'running') icon = '⏳'; + else if (st === 'failed') icon = '❌'; + h += ''; + } + h += ''; + } + h += '
FileStatus' + s.slice(0,4) + '
' + name + '' + statusIcon + ' ' + (f.job_status || f.status) + '' + icon + '
'; + el.innerHTML = h; +} + function renderDb(d) { if (!d) return; const rows = ['videos','chunks','face_detections','identities','tkg_nodes','tkg_edges']; @@ -316,6 +407,64 @@ function renderDb(d) { document.getElementById('db').innerHTML = h; } +let _lastData = null; +function copyStatus() { + if (!_lastData) { alert('No data loaded yet'); return; } + const d = _lastData; + const job = d.status?.job; + const h = d.status?.health; + const db = d.db; + const r = d.redis; + let lines = []; + lines.push('Momentry Pipeline Status'); + lines.push('='.repeat(50)); + lines.push('System: ' + (d.system?.role || '?') + ' | ' + new Date().toISOString().slice(0,19).replace('T',' ')); + lines.push(''); + if (job?.stages) { + lines.push('── Checklist ──'); + for (const s of job.stages) { + lines.push(' ' + (s.passed ? '✅' : '❌') + ' ' + s.name.padEnd(14) + s.detail); + } + lines.push(' ' + (job.passed ? '✅' : '❌') + ' TOTAL'.padEnd(14) + job.total_elapsed + 's'); + lines.push(''); + } + if (h) { + lines.push('── Health ──'); + lines.push(' CPU: ' + (h.cpu_load_1m ?? '?') + ' Memory: ' + (h.memory_used_mb ?? '?') + 'MB GPU: ' + (h.gpu_available ? '✅' : '❌')); + if (h.services) { + lines.push(' Services: ' + Object.entries(h.services).map(([k,v]) => k + '=' + (v ? '✓' : '✗')).join(' ')); + } + lines.push(''); + } + if (r) { + lines.push('── Redis ──'); + lines.push(' Keys: ' + (r.momentry_keys ?? '?') + ' Hit Rate: ' + (r.hit_rate_pct ?? '?') + '% Uptime: ' + (r.uptime_in_seconds ? Math.round(r.uptime_in_seconds/3600)+'h' : '?')); + lines.push(''); + } + if (db) { + lines.push('── Database ──'); + const tbls = ['videos','chunks','face_detections','identities','tkg_nodes','tkg_edges']; + for (const t of tbls) { + if (db[t] !== undefined) lines.push(' ' + t + ': ' + db[t].toLocaleString()); + } + if (db.files) { + lines.push(''); + lines.push('── Files ──'); + for (const f of db.files) { + lines.push(' ' + (f.job_status === 'running' ? '▶️' : f.job_status === 'pending' ? '⏳' : f.status === 'completed' ? '✅' : '❌') + ' ' + f.name.slice(0,60)); + } + } + lines.push(''); + } + const text = lines.join('\n'); + navigator.clipboard.writeText(text).then(() => { + const btn = event.target; + const orig = btn.textContent; + btn.textContent = '✅ Copied!'; + setTimeout(() => btn.textContent = orig, 2000); + }).catch(() => alert('Copy failed')); +} + fetchAll(); setInterval(fetchAll, 15000);