dashboard: copy button, dedup files, /api/all single call
This commit is contained in:
@@ -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; }
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="refresh-bar">
|
||||
<h1>Momentry Dashboard</h1>
|
||||
<div>
|
||||
<div class="refresh-bar">
|
||||
<h1>Momentry Dashboard <span style="font-size:14px;background:#1f2937;color:#{{'58a6ff' if IS_M5 else 'f0883e'}};padding:4px 12px;border-radius:12px;margin-left:8px;vertical-align:middle">🤖 {{ SYSTEM_ROLE }}</span></h1>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span class="last-updated" id="lastUpdated">—</span>
|
||||
<button onclick="fetchAll()" style="margin-left:12px">⟳ Refresh</button>
|
||||
<button onclick="copyStatus()" style="background:#1f6feb;padding:6px 14px;font-size:13px">📋 Copy</button>
|
||||
<button onclick="fetchAll()" style="background:#238636;padding:6px 14px;font-size:13px">⟳ Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,6 +239,11 @@ button:hover { background: #2ea043; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="fileProgressSection">
|
||||
<h2>📁 Pipeline Progress</h2>
|
||||
<div id="fileProgress" style="font-size:14px">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="section">
|
||||
@@ -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 = '<tr><td class="fail">Error: ' + e.message + '</td></tr>';
|
||||
// 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 = '<div style="color:#8b949e">No files found</div>';
|
||||
return;
|
||||
}
|
||||
let h = '<table><tr><th>File</th><th>Status</th>';
|
||||
for (const s of PIPELINE_STAGES) h += '<th style="font-size:11px">' + s.slice(0,4) + '</th>';
|
||||
h += '</tr>';
|
||||
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 += '<tr><td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + f.name + '">' + name + '</td>'
|
||||
+ '<td>' + statusIcon + ' ' + (f.job_status || f.status) + '</td>';
|
||||
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 += '<td style="text-align:center;font-size:13px">' + icon + '</td>';
|
||||
}
|
||||
h += '</tr>';
|
||||
}
|
||||
h += '</table>';
|
||||
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);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user