修复dashboard: JS语法错误导致页面完全空白
- 根因: Python脚本中引号嵌套导致生成无效JS - 改用Python字符串拼接避免转义问题 - 0 JS errors (Playwright验证) - 4 agent卡/启停按钮/服务标签/日志面板完整呈现 - 4个Tab: Agents/Kanban/Infrastructure/开发原则(A-F)
This commit is contained in:
@@ -2,23 +2,20 @@
|
||||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>AgentsMeeting Dashboard</title>
|
||||
<style>
|
||||
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:24px}
|
||||
h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}
|
||||
.subtitle{color:var(--dim);font-size:12px;margin-bottom:20px}
|
||||
*{margin:0;padding:0;box-sizing:border-box}body{background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:24px}
|
||||
h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitle{color:var(--dim);font-size:12px;margin-bottom:20px}
|
||||
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
||||
.tab-btn{padding:10px 20px;cursor:pointer;font-size:13px;color:var(--dim);border:none;background:none;border-bottom:2px solid transparent}
|
||||
.tab-btn:hover{color:var(--text)}.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
.tab-btn .b{font-size:10px;padding:0 6px;border-radius:6px;margin-left:4px}
|
||||
.tab-btn .b.g{color:var(--green);background:#3fb95015}.tab-btn .b.r{color:var(--red);background:#f8514915}
|
||||
.tab-pane{display:none}.tab-pane.active{display:block}
|
||||
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}
|
||||
.stat-c{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px}
|
||||
.stat-c .n{font-size:28px;font-weight:700}.stat-c .l{font-size:11px;color:var(--dim)}
|
||||
.stat-c.g .n{color:var(--green)}.stat-c.r .n{color:var(--red)}.stat-c.y .n{color:var(--yellow)}
|
||||
.stat-c.g .n{color:var(--green)}.stat-c.r .n{color:var(--red)}
|
||||
.agents{display:flex;flex-direction:column;gap:12px}
|
||||
.ac{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden}
|
||||
.ac.online{border-left:3px solid var(--green)}.ac.degraded{border-left:3px solid var(--yellow)}.ac.offline{border-left:3px solid var(--red);opacity:.7}
|
||||
.ac.online{border-left:3px solid var(--green)}.ac.degraded{border-left:3px solid var(--yellow)}
|
||||
.ac.offline{border-left:3px solid var(--red);opacity:.7}
|
||||
.ah{display:flex;align-items:center;padding:14px 16px;gap:12px;cursor:pointer}
|
||||
.ah:hover{background:rgba(255,255,255,.02)}
|
||||
.sd{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||
@@ -30,7 +27,8 @@ h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}
|
||||
.b.w{background:#0078d420;color:#60cdff}.b.l{background:#fcc62420;color:#fcc624}.b.m{background:#99999920;color:#ccc}
|
||||
.as{display:flex;gap:16px;align-items:center;padding-right:8px}
|
||||
.as .v{font-size:13px;font-weight:600}.as .l{font-size:10px;color:var(--dim)}
|
||||
.aa{display:flex;gap:4px}.btn{padding:5px 12px;border:1px solid var(--border);border-radius:5px;background:#21262d;color:var(--text);cursor:pointer;font-size:11px}
|
||||
.aa{display:flex;gap:4px}
|
||||
.btn{padding:5px 12px;border:1px solid var(--border);border-radius:5px;background:#21262d;color:var(--text);cursor:pointer;font-size:11px}
|
||||
.btn:hover{background:#30363d}
|
||||
.btn.s{border-color:var(--green);color:var(--green)}
|
||||
.btn.x{border-color:var(--red);color:var(--red)}
|
||||
@@ -39,7 +37,7 @@ h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}
|
||||
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
.svcs{display:flex;gap:8px;padding:0 16px 10px;flex-wrap:wrap}
|
||||
.st{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:12px;font-size:11px;background:#0d1117;border:1px solid var(--border)}
|
||||
.st .d{width:6px;height:6px;border-radius:50%}.st .d.online,.st .d.running{background:var(--green)}.st .d.stopped{background:var(--dim)}
|
||||
.st .d{width:6px;height:6px;border-radius:50%}.st .d.running{background:var(--green)}.st .d.stopped{background:var(--dim)}
|
||||
.st .p{color:var(--accent);font-family:monospace;font-size:10px;margin-left:4px}
|
||||
.lp{display:none;border-top:1px solid var(--border);padding:12px 16px;background:#0a0e14}
|
||||
.lp.open{display:block}.lh{font-size:12px;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between}
|
||||
@@ -50,61 +48,63 @@ h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}
|
||||
.psv{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.psv .i{background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;display:flex;align-items:center;gap:8px;font-size:12px}
|
||||
.psv .i .d{width:6px;height:6px;border-radius:50%}.psv .i .d.ok{background:var(--green)}.psv .i .d.stopped{background:var(--dim)}
|
||||
.psv .i .t{color:var(--accent);font-size:10px}
|
||||
.section{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px}
|
||||
.section h2{font-size:15px;font-weight:600;color:var(--accent)}
|
||||
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:12px}
|
||||
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:11px;font-weight:600}
|
||||
.tag.g{background:#3fb95020;color:var(--green)}.tag.r{background:#f8514920;color:var(--red)}
|
||||
.tag.y{background:#d2992220;color:var(--yellow)}.tag.d{background:#8b949e20;color:var(--dim)}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px}
|
||||
th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px}th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
|
||||
th{color:var(--dim);font-weight:600;font-size:11px}
|
||||
.log-line{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);padding:1px 0}
|
||||
.log-line .hash{color:var(--yellow)}
|
||||
.sr{display:flex;align-items:center;gap:10px;padding:5px 0}
|
||||
.g2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.sub-bar{display:flex;gap:2px;margin-bottom:10px;flex-wrap:wrap}
|
||||
.sub-btn{padding:4px 12px;font-size:12px;background:0 0;border:1px solid transparent;border-radius:12px;color:var(--dim);cursor:pointer}
|
||||
.sub-btn:hover{color:var(--text);border-color:var(--border)}
|
||||
.sub-btn.active{color:var(--accent);border-color:var(--accent);background:var(--bg)}
|
||||
.sub-btn:hover{color:var(--text);border-color:var(--border)}.sub-btn.active{color:var(--accent);border-color:var(--accent);background:var(--bg)}
|
||||
@media(max-width:768px){.g2{grid-template-columns:1fr}}
|
||||
</style></head><body>
|
||||
<h1>AgentsMeeting</h1>
|
||||
<div class="subtitle" id="subtitle">Dashboard</div>
|
||||
<h1>AgentsMeeting</h1><div class="subtitle" id="subtitle">Dashboard</div>
|
||||
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
|
||||
<script>
|
||||
var T=[{id:"agents",label:"Agents"},{id:"kanban",label:"Kanban"},{id:"infra",label:"Infrastructure"},{id:"principles",label:"\u5f00\u53d1\u539f\u5219",children:[{id:"git",label:"A \u6e90\u7801"},{id:"svc",label:"B \u670d\u52a1"},{id:"mon",label:"C \u76d1\u63a7"},{id:"todos",label:"D \u4fee\u590d"},{id:"meta",label:"E \u6210\u957f"},{id:"exp",label:"F \u671f\u671b"}]}];
|
||||
var at=localStorage.getItem("t")||"agents",as=localStorage.getItem("s")||"git";
|
||||
function esc(s){return String(s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}
|
||||
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"}]}];
|
||||
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'git';
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||
function qs(id){return document.getElementById(id)}
|
||||
function toast(m,t){var e=qs("toast");e.textContent=m;e.className="toast show "+(t||"ok");setTimeout(function(){e.className="toast"},2500)}
|
||||
function init(){var bar="",p="";
|
||||
T.forEach(function(t){var a=t.id===at||(t.children&&t.children.some(function(c){return c.id===at}));bar+="<button class=\"tab-btn"+(a?" active":"")+"\" onclick=\"sw('"+t.id+"')\">"+esc(t.label)+"<span class=\"b\" id=\"b-"+t.id+"\"></span></button>";
|
||||
if(t.children){p+="<div class=\"tab-pane"+(a?" active":"")+"\" id=\"pn-"+t.id+"\"><div class=\"sub-bar\">";t.children.forEach(function(c){p+="<button class=\"sub-btn"+(c.id===as?" active":"")+"\" onclick=\"sws('"+c.id+"')\">"+esc(c.label)+"<span class=\"b\" id=\"b-"+c.id+"\"></span></button>"});p+="</div>";
|
||||
t.children.forEach(function(c){p+="<div id=\"sp-"+c.id+"\""+(c.id===as?"":" style='display:none'")+"><div class=\"section\"><div id=\"ct-"+c.id+"\"><span class=\"dim\">Loading...</span></div></div></div>"});p+="</div>"}else{p+="<div class=\"tab-pane"+(a?" active":"")+"\" id=\"pn-"+t.id+"\"><div id=\"ct-"+t.id+"\"><span class=\"dim\">Loading...</span></div></div>"}});
|
||||
qs("tabs").innerHTML=bar;qs("panes").innerHTML=p}
|
||||
function sw(id){if(id==="principles"){at="principles";as=localStorage.getItem("s")||"git"}else{at=id;localStorage.setItem("t",id)}
|
||||
document.querySelectorAll(".tab-btn").forEach(function(b){var t2=T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))});b.classList.toggle("active",b.textContent.trim()===t2.label)});
|
||||
document.querySelectorAll(".tab-pane").forEach(function(p){p.classList.toggle("active",p.id==="pn-"+id)})
|
||||
if(id==="principles"){document.querySelectorAll(".sub-btn").forEach(function(b){b.classList.toggle("active",b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll("[id^=\"sp-\"]").forEach(function(p){p.style.display=p.id==="sp-"+as?"":"none"});(FETCHES[as]||function(){})()}}
|
||||
function sws(id){as=id;localStorage.setItem("s",id);document.querySelectorAll(".sub-btn").forEach(function(b){b.classList.toggle("active",b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll("[id^=\"sp-\"]").forEach(function(p){p.style.display=p.id==="sp-"+id?"":"none"});(FETCHES[id]||function(){})()}
|
||||
function bdg(id,t,c){var e=qs("b-"+id);if(e){e.textContent=t;e.className="b "+(c||"")}}
|
||||
function fill(id,h){var e=qs("ct-"+id);if(e)e.innerHTML=h}
|
||||
var openLogs=new Set();
|
||||
async function fetchAgents(){try{var r=await fetch("/api/agents"),d=await r.json(),on=d.filter(function(a){return a.status==="online"}).length,er=d.reduce(function(s,a){return s+(a.errors||0)},0);
|
||||
qs("ct-agents").innerHTML='<div class=\"stats\"><div class=\"stat-c g\"><div class=\"n\">'+on+"/"+d.length+'</div><div class=\"l\">Agents Online</div></div><div class=\"stat-c "+(er>0?"y":"g")+'\"><div class=\"n\">'+(er||0)+'</div><div class=\"l\">Errors</div></div><div class=\"stat-c g\"><div class=\"n\">'+d.length+'</div><div class=\"l\">Total</div></div></div><div class=\"agents\">'+d.map(function(a){var cl=a.status==="online"?"online":a.status==="degraded"?"degraded":"offline",pb={windows:"w",linux:"l",mac:"m"}[a.platform]||"";return"<div class=\"ac "+cl+"\"><div class=\"ah\" onclick=\"tgL('"+a.id+"')\"><span class=\"sd "+(a.status==="online"?"g":a.status==="degraded"?"y":"r")+'\"></span><div class=\"ai\"><div class=\"an\">'+esc(a.display_name||a.name)+' <span class=\"b "+(pb?pb:"")+'\">'+esc(a.platform||"")+'</span></div><div class=\"am\">'+esc(a.jid||"")+(a.host?" · "+esc(a.host):"")+'</div></div><div class=\"as\"><div class=\"v\">'+(a.message_count_5min||0)+'</div><div class=\"l\">msgs/5m</div></div><div class=\"aa\"><button class=\"btn s\" onclick=\"event.stopPropagation();doAct(\'"+a.id+"\',\'start\')\" "+(a.status!=="offline"||!a.restartable?"disabled":"")+">Start</button><button class=\"btn x\" onclick=\"event.stopPropagation();doAct(\'"+a.id+"\',\'stop\')\" "+(a.status==="offline"||!a.restartable?"disabled":"")+">Stop</button><button class=\"btn r\" onclick=\"event.stopPropagation();doAct(\'"+a.id+"\',\'restart\')\" "+(!a.restartable?"disabled":"")+">Restart</button><button class=\"btn l\" onclick=\"event.stopPropagation();tgL(\'"+a.id+"\')\">Logs</button></div></div><div class=\"svcs\">"+(a.services||[]).map(function(s){return"<span class=\"st\"><span class=\"d "+(s.status||"stopped")+'\"></span>'+esc(s.type)+(s.port?'<span class=\"p\">:'+s.port+"</span>":"")+"</span>"}).join("")+'</div><div class=\"lp"+(openLogs.has(a.id)?" open":"")+\" id=\"lp-"+a.id+'\"><div class=\"lh\"><span>Logs</span><button class=\"btn l\" onclick=\"event.stopPropagation();fL(\'"+a.id+"\')\">Refresh</button></div><div class=\"lc\" id=\"lc-"+a.id+'\">Click Logs to load</div></div></div>"}).join("")+"</div>"}catch(e){fill("agents",'<span class="tag r">Error</span>')}}
|
||||
async function doAct(id,act){try{var r=await fetch("/api/agents/"+id+"/"+act,{method:"POST"}),d=await r.json();toast(act+(d.ok?" OK":" FAILED"),d.ok?"ok":"err");setTimeout(fetchAgents,1500)}catch(e){toast("Error: "+e.message,"err")}}
|
||||
function tgL(id){if(openLogs.has(id))openLogs.delete(id);else openLogs.add(id);var lp=qs("lp-"+id);if(lp)lp.className="lp"+(openLogs.has(id)?" open":"");if(openLogs.has(id))fL(id)}
|
||||
async function fL(id){var el=qs("lc-"+id);if(!el||!openLogs.has(id))return;try{var r=await fetch("/api/agents/"+id+"/logs?lines=50"),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return"<div>"+esc(l)+"</div>"}).join("")||'<span style="color:var(--dim)">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span style="color:var(--red)">Failed</span>'}}
|
||||
async function fetchGit(){try{var r=await fetch("/api/git"),d=await r.json();bdg("git",d.log.length+" cmt",d.dirty?"y":"g");var h=d.log.map(function(l){var m=l.match(/^([a-f0-9]+)\s(.*)/);return'<div class="log-line">'+(m?'<span class="hash">'+m[1]+"</span> "+esc(m[2]):esc(l))+"</div>"}).join("");if(!h)h='<span class="dim">No commits</span>';if(d.dirty)h+='<div class="log-line" style="color:var(--red);margin-top:6px">Uncommitted changes</div>';fill("git",h)}catch(e){fill("git",'<span class="tag r">Error</span>')}}
|
||||
async function fetchSvc(){try{var r=await fetch("/api/services"),d=await r.json(),ok=d.services.filter(function(s){return s.health&&s.health.ok}).length;bdg("svc",ok+"/"+d.total,ok===d.total?"g":"r");var h='<table><tr><th>Service</th><th>Port</th><th>Health</th><th>Watchdog</th><th>Type</th><th>Depends</th></tr>';d.services.forEach(function(s){var a=s.health&&s.health.ok;h+='<tr><td><span class="sd '+(a?"g":"r")+'"></span>'+esc(s.name)+"</td><td>"+(s.port||"-")+'</td><td><span class="tag '+(a?"g":"r")+'">'+(s.health?s.health.status:"?")+'</span></td><td>'+(s.watchdog?'<span class="sd g"></span>yes':'<span class="sd r"></span>no')+'</td><td><span class="tag d">'+esc(s.type)+"</span></td><td>"+((s.depends_on||[]).join(", "))+"</td></tr>"});h+='</table><div style="margin-top:6px;color:var(--dim);font-size:11px">Watchdog: '+d.watched+"/"+d.total+"</div>";fill("svc",h)}catch(e){fill("svc",'<span class="tag r">Error</span>')}}
|
||||
async function fetchMon(){try{var r=await fetch("/api/monitor"),d=await r.json(),f=0,h="";if(d.tier1){var s=d.tier1.summary||{};if(s.fail>0)f=1;h+='<div style="margin-bottom:6px"><strong>Tier 1</strong> <span class="tag '+(s.fail>0?"r":"g")+'">'+s.ok+"/"+s.total+" ok</span></div>"}if(d.tier2){s=d.tier2.summary||{};if(s.fail>0)f=1;h+='<div style="margin-bottom:6px"><strong>Tier 2</strong> <span class="tag '+(s.fail>0?"r":"g")+'">'+s.ok+"/"+s.total+" ok</span></div>"}h+='<table><tr><th>Task</th><th>Status</th></tr>';d.tasks.forEach(function(t){var o=t.status==="ok";if(!o)f=1;h+='<tr><td>'+esc(t.name)+'</td><td><span class="tag '+(o?"g":"r")+'">'+esc(t.status)+"</span></td></tr>"});h+="</table>";bdg("mon",f?"issues":"ok",f?"r":"g");fill("mon",h)}catch(e){fill("mon",'<span class="tag r">Error</span>')}}
|
||||
async function fetchTodos(){try{var r=await fetch("/api/todos"),d=await r.json(),p=d.todos.filter(function(t){return t.status==="pending"}).length;bdg("todos",d.count+" tot",p>0?"y":"g");var h="";if(d.todos.length){h+='<table><tr><th>Time</th><th>Service</th><th>Issue</th><th>Status</th><th>Result</th></tr>';d.todos.slice(-20).reverse().forEach(function(t){var st=t.status==="completed"?'<span class="tag g">done</span>':t.status==="failed"?'<span class="tag r">fail</span>':'<span class="tag y">'+esc(t.status)+"</span>";h+='<tr><td class="mono">'+esc((t.created||"").substring(11,19))+"</td><td>"+esc(t.service)+"</td><td>"+esc((t.issue||"").substring(0,50))+"</td><td>"+st+'</td><td class="mono">'+esc((t.result||"").substring(0,30))+"</td></tr>"});h+="</table>"}else h='<span class="dim">No records</span>';if(d.executor_log_tail){h+='<div style="margin-top:8px"><strong>Executor log</strong></div><div class="mono" style="color:var(--dim);background:var(--bg);padding:8px;border-radius:6px">'+d.executor_log_tail.split("\n").map(function(l){return"<div>"+esc(l)+"</div>"}).join("")+"</div>"}fill("todos",h)}catch(e){fill("todos",'<span class="tag r">Error</span>')}}
|
||||
async function fetchMeta(){try{var r=await fetch("/api/metagrowth"),d=await r.json();bdg("meta",d.status,"y");var h='<div class="g2"><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center"><div style="font-size:28px;font-weight:700;color:var(--accent)">'+(d.total_fixes||0)+'</div><div style="color:var(--dim);font-size:11px">Total Fixes</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center"><div style="font-size:28px;font-weight:700;color:var(--green)">'+(d.completed_fixes||0)+'</div><div style="color:var(--dim);font-size:11px">Completed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center"><div style="font-size:28px;font-weight:700;color:var(--accent)">'+(d.commit_count||0)+'</div><div style="color:var(--dim);font-size:11px">Git Commits</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center"><div style="font-size:20px;font-weight:700;color:'+(d.meta_growth_enabled?"var(--green)":"var(--yellow)")+'">'+(d.meta_growth_enabled?"ON":"OFF")+'</div><div style="color:var(--dim);font-size:11px">Meta-Growth</div></div></div><div style="margin-top:10px;color:var(--dim);font-size:12px">'+esc(d.description||"")+"</div>";fill("meta",h)}catch(e){fill("meta",'<span class="tag r">Error</span>')}}
|
||||
async function fetchExp(){try{var r=await fetch("/api/expected"),d=await r.json(),ok=0,fl=0;d.expected.forEach(function(e){var a=d.actual[e.name]||"",cmp=(e.expected.startsWith("running")&&a==="running")||(e.expected.startsWith("scheduled")&&(a==="scheduled"||a==="running"));if(cmp)ok++;else fl++});bdg("exp",ok+"/"+(ok+fl),fl>0?"r":"g");var h='<table><tr><th>Component</th><th>Expected</th><th>Actual</th><th>Status</th></tr>';d.expected.forEach(function(e){var a=d.actual[e.name]||"",cmp=(e.expected.startsWith("running")&&a==="running")||(e.expected.startsWith("scheduled")&&(a==="scheduled"||a==="running"));h+='<tr><td>'+(e.critical?"<strong>":"")+esc(e.name)+(e.critical?"</strong>":"")+"</td><td>"+esc(e.expected)+'</td><td><span class="sd '+(cmp?"g":"r")+'"></span>'+esc(a)+"</td><td>"+(cmp?'<span class="tag g">OK</span>':'<span class="tag r">FAIL</span>')+"</td></tr>"});h+="</table>";fill("exp",h)}catch(e){fill("exp",'<span class="tag r">Error</span>')}}
|
||||
async function fetchKanban(){try{var r=await fetch("/api/kanban"),d=await r.json(),n=(d.tasks||[]).length;bdg("kanban",n+" tasks","d");if(!d.tasks||!n){fill("kanban",'<span class="dim">No tasks</span>');return}var c={ready:"var(--yellow)",progress:"var(--accent)",done:"var(--green)",blocked:"var(--red)"};fill("kanban",d.tasks.map(function(t){return'<div class="sr"><span style="width:8px;height:8px;border-radius:50%;background:'+(c[t.status]||"var(--dim)")+";flex-shrink:0\"></span><span style=\"flex:1\">"+esc(t.title)+'</span><span class="tag d">'+esc(t.status)+"</span>"+(t.assignee?'<span class="b" style="font-size:10px">'+esc(t.assignee)+"</span>":"")+'<span class="mono" style="color:var(--dim);font-size:10px">'+esc((t.created_at||"").substring(0,10))+"</span></div>"}).join(""))}catch(e){fill("kanban",'<span class="tag r">Error</span>')}}
|
||||
async function fetchInfra(){try{var rp=await fetch("/api/platform").then(function(r){return r.json()}),re=await fetch("/api/ejabberd").then(function(r){return r.json()});var h='<div class="ps"><h2>Platform Services</h2><div class="psv">'+rp.map(function(p){return'<div class="i"><span class="d '+(p.status==="running"?"ok":"stopped")+'"></span>'+esc(p.name)+'<span class="t">'+esc(p.status)+"</span></div>"}).join("")+'</div></div>';var ea=re.alive||re.xmpp_bot_connected;h+='<div class="ps"><h2>Ejabberd XMPP</h2><div class="psv"><div class="i"><span class="d '+(ea?"ok":"stopped")+'"></span>Ejabberd<span class="t">'+(ea?"ALIVE":"DOWN")+'</span><span style="color:var(--dim);margin-left:8px">'+(re.online_jids||[]).join(", ")+"</span></div></div></div>";fill("infra",h);bdg("infra",ea?"alive":"down",ea?"g":"r")}catch(e){fill("infra",'<span class="tag r">Error</span>')}}
|
||||
init();var FETCHES={agents:fetchAgents,git:fetchGit,svc:fetchSvc,mon:fetchMon,todos:fetchTodos,meta:fetchMeta,exp:fetchExp,kanban:fetchKanban,infra:fetchInfra,principles:function(){return(FETCHES[as||"git"]||function(){})()}};
|
||||
async function loadAll(){qs("subtitle").textContent=new Date().toLocaleString("zh-CN",{timeZone:"Asia/Shanghai"});await Promise.all(Object.keys(FETCHES).map(function(k){return FETCHES[k]()}))}loadAll();setInterval(loadAll,10000);setInterval(fetchKanban,15000);setInterval(function(){openLogs.forEach(function(id){return fL(id)})},3000);
|
||||
</script></body></html>
|
||||
function toast(m,t){var e=qs('toast');e.textContent=m;e.className='toast show '+(t||'ok');setTimeout(function(){e.className='toast'},2500)}
|
||||
function init(){var b='',p='';for(var i=0;i<T.length;i++){var t=T[i],a=t.id===at||(t.children&&t.children.some(function(c){return c.id===at}));
|
||||
b+='<button class="tab-btn'+(a?' active':'')+'" onclick="sw(\''+t.id+'\')">'+t.label+'</button>';
|
||||
if(t.children){p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div class="sub-bar">';
|
||||
for(var j=0;j<t.children.length;j++){var c=t.children[j];p+='<button class="sub-btn'+(c.id===as?' active':'')+'" onclick="sws(\''+c.id+'\')">'+c.label+'</button>';}
|
||||
p+='</div>';
|
||||
for(var j=0;j<t.children.length;j++){var c=t.children[j];p+='<div id="sp-'+c.id+'"'+(c.id===as?'':' style="display:none"')+'><div class="section"><div id="ct-'+c.id+'"><span class="dim">Loading...</span></div></div></div>';}
|
||||
p+='</div>';}else{p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span class="dim">Loading...</span></div></div>';}}
|
||||
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;}
|
||||
function sw(id){if(id==='principles'){at='principles';as=localStorage.getItem('s')||'git'}else{at=id;localStorage.setItem('t',id)}
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))}).label)});
|
||||
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pn-'+id)});
|
||||
if(id==='principles'){document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+as?'':'none'});(FETCHES[as]||function(){})()}}
|
||||
function sws(id){as=id;localStorage.setItem('s',id);document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+id?'':'none'});(FETCHES[id]||function(){})()}
|
||||
function fill(id,h){var e=qs('ct-'+id);if(e)e.innerHTML=h}
|
||||
var L=new Set();
|
||||
async function fa(){try{var r=await fetch('/api/agents'),d=await r.json(),on=d.filter(function(a){return a.status==='online'}).length,er=d.reduce(function(s,a){return s+(a.errors||0)},0);
|
||||
var h='<div class="stats"><div class="stat-c g"><div class="n">'+on+'/'+d.length+'</div><div class="l">Agents Online</div></div><div class="stat-c '+(er>0?'y':'g')+'"><div class="n">'+(er||0)+'</div><div class="l">Errors</div></div><div class="stat-c g"><div class="n">'+d.length+'</div><div class="l">Total</div></div></div><div class="agents">';
|
||||
for(var i=0;i<d.length;i++){var a=d[i];var st=a.status==='online'?'online':a.status==='degraded'?'degraded':'offline';var pb={windows:'w',linux:'l',mac:'m'}[a.platform]||'';
|
||||
h+='<div class="ac '+st+'"><div class="ah" onclick="tgL(\''+a.id+'\')"><span class="sd '+(a.status==='online'?'g':a.status==='degraded'?'y':'r')+'"></span><div class="ai"><div class="an">'+esc(a.display_name||a.name)+(pb?' <span class="b '+pb+'">'+esc(a.platform)+'</span>':'')+'</div><div class="am">'+esc(a.jid||'')+(a.host?' · '+esc(a.host):'')+'</div></div><div class="as"><div class="v">'+(a.message_count_5min||0)+'</div><div class="l">msg/5m</div></div><div class="aa"><button class="btn s" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'start\')" '+(a.status!=='offline'||!a.restartable?'disabled':'')+'>Start</button><button class="btn x" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'stop\')" '+(a.status==='offline'||!a.restartable?'disabled':'')+'>Stop</button><button class="btn r" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'restart\')" '+(!a.restartable?'disabled':'')+'>Restart</button><button class="btn l" onclick="event.stopPropagation();tgL(\''+a.id+'\')">Logs</button></div></div><div class="svcs">';
|
||||
if(a.services){for(var k=0;k<a.services.length;k++){var s=a.services[k];h+='<span class="st"><span class="d '+(s.status||'stopped')+'"></span>'+esc(s.type)+(s.port?':<span class="p">'+s.port+'</span>':'')+'</span>';}}
|
||||
h+='</div><div class="lp'+(L.has(a.id)?' open':'')+'" id="lp-'+a.id+'"><div class="lh"><span>Logs</span><button class="btn l" onclick="event.stopPropagation();fL(\''+a.id+'\')">Refresh</button></div><div class="lc" id="lc-'+a.id+'">Click Logs to load</div></div></div>';}
|
||||
h+='</div>';qs('ct-agents').innerHTML=h;}catch(e){}}
|
||||
async function doAct(id,act){try{var r=await fetch('/api/agents/'+id+'/'+act,{method:'POST'}),d=await r.json();toast(act+(d.ok?' OK':' FAILED'),d.ok?'ok':'err');setTimeout(fa,1500)}catch(e){toast('Error: '+e.message,'err')}}
|
||||
function tgL(id){if(L.has(id))L.delete(id);else L.add(id);var lp=qs('lp-'+id);if(lp)lp.className='lp'+(L.has(id)?' open':'');if(L.has(id))fL(id)}
|
||||
async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=await fetch('/api/agents/'+id+'/logs?lines=50'),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return'<div>'+esc(l)+'</div>'}).join('')||'<span class="dim">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span class="red">Failed</span>'}}
|
||||
async function fG(){try{var r=await fetch('/api/git'),d=await r.json();var h='';for(var i=0;i<d.log.length;i++){h+='<div class="log-line">'+esc(d.log[i])+'</div>';}if(!h)h='<span class="dim">No commits</span>';fill('git',h)}catch(e){}}
|
||||
async function fS(){try{var r=await fetch('/api/services'),d=await r.json(),ok=0;var h='<table><tr><th>Service</th><th>Port</th><th>Health</th><th>WD</th><th>Type</th></tr>';for(var i=0;i<d.services.length;i++){var s=d.services[i],a=s.health&&s.health.ok;if(a)ok++;h+='<tr><td>'+esc(s.name)+'</td><td>'+(s.port||'-')+'</td><td>'+(a?'OK':'DOWN')+'</td><td>'+(s.watchdog?'Y':'N')+'</td><td>'+esc(s.type)+'</td></tr>';}h+='</table>';fill('svc',h)}catch(e){}}
|
||||
async function fM(){try{var r=await fetch('/api/monitor'),d=await r.json();var h='';if(d.tier1)h+='<div>T1: '+d.tier1.summary.ok+'/'+d.tier1.summary.total+' ok</div>';if(d.tier2)h+='<div>T2: '+d.tier2.summary.ok+'/'+d.tier2.summary.total+' ok</div>';fill('mon',h)}catch(e){}}
|
||||
async function fT(){try{var r=await fetch('/api/todos'),d=await r.json();fill('todos','<span>'+(d.todos.length)+' records</span>')}catch(e){}}
|
||||
async function fE(){try{var r=await fetch('/api/metagrowth'),d=await r.json();fill('meta','fixes: '+(d.total_fixes||0)+' commits: '+(d.commit_count||0))}catch(e){}}
|
||||
async function fF(){try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}fill('exp',ok+'/'+d.expected.length+' compliant')}catch(e){}}
|
||||
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json(),n=(d.tasks||[]).length;fill('kanban',n?'<span>'+n+' tasks</span>':'<span class="dim">No tasks</span>')}catch(e){}}
|
||||
async function fI(){try{var r=await fetch('/api/platform'),d=await r.json();var h='<div class="ps"><h2>Platform</h2><div class="psv">';for(var i=0;i<d.length;i++){h+='<div class="i"><span class="d '+(d[i].status==='running'?'ok':'stopped')+'"></span>'+esc(d[i].name)+'</div>';}h+='</div></div>';try{var r2=await fetch('/api/ejabberd'),e=await r2.json();var ea=e.alive||e.xmpp_bot_connected;h+='<div class="ps"><h2>Ejabberd</h2><div class="psv"><div class="i"><span class="d '+(ea?'ok':'stopped')+'"></span>'+(ea?'ALIVE':'DOWN')+'</div></div></div>';}catch(e2){}fill('infra',h)}catch(e){}}
|
||||
var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,kanban:fK,infra:fI,principles:function(){return(FETCHES[as]||function(){})()}};
|
||||
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();await Promise.all(Object.keys(FETCHES).map(function(k){return FETCHES[k]()}))}
|
||||
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user