恢复正式版agent控制面板 + 开发原则父Tab
- Agents Tab: 完整agent卡/启停按钮/服务标签/日志查看 - Kanban Tab: 看板任务 - Infrastructure Tab: 平台服务+Ejabberd - 开发原则 Tab: A-F子Tab(A源码/B服务/C监控/D修复/E成长/F期望) - 排在后位,不喧宾夺主
This commit is contained in:
@@ -1,148 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AgentsMeeting Dashboard</title>
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
|
||||
body{background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:20px}
|
||||
h1{font-size:22px;font-weight:600;color:var(--accent);margin-bottom:2px}
|
||||
.subtitle{color:var(--dim);font-size:12px;margin-bottom:16px}
|
||||
.tab-bar{display:flex;gap:4px;margin-bottom:12px;flex-wrap:wrap;border-bottom:1px solid var(--border)}
|
||||
.tab-btn{padding:8px 16px;font-size:13px;font-weight:500;background:0 0;border:none;color:var(--dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;white-space:nowrap}
|
||||
.tab-btn:hover{color:var(--text);background:rgba(255,255,255,.03)}
|
||||
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
.tab-btn .badge{font-size:10px;padding:1px 7px;border-radius:8px;background:var(--bg);margin-left:6px;font-weight:400}
|
||||
.tab-btn .badge.green{color:var(--green);background:#3fb95015}
|
||||
.tab-btn .badge.red{color:var(--red);background:#f8514915}
|
||||
.tab-btn .badge.yellow{color:var(--yellow);background:#d2992215}
|
||||
.sub-bar{display:flex;gap:2px;margin-bottom:10px;flex-wrap:wrap;padding-left:4px}
|
||||
.sub-btn{padding:4px 12px;font-size:12px;font-weight:400;background:0 0;border:1px solid transparent;border-radius:12px;color:var(--dim);cursor:pointer;transition:all .15s}
|
||||
.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 .badge{font-size:9px;padding:0 6px;border-radius:6px;margin-left:4px;font-weight:400}
|
||||
.sub-btn .badge.green{color:var(--green);background:#3fb95015}
|
||||
.sub-btn .badge.red{color:var(--red);background:#f8514915}
|
||||
.sub-btn .badge.yellow{color:var(--yellow);background:#d2992215}
|
||||
.tab-panel{display:none}
|
||||
.tab-panel.active{display:block}
|
||||
*{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)}
|
||||
.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}
|
||||
.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}
|
||||
.sd.g{background:var(--green);box-shadow:0 0 6px var(--green)}
|
||||
.sd.y{background:var(--yellow)}.sd.r{background:var(--red)}
|
||||
.ai{flex:1;min-width:0}.an{font-size:15px;font-weight:600}
|
||||
.am{font-size:12px;color:var(--dim);margin-top:2px}
|
||||
.b{display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;margin-left:8px}
|
||||
.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}
|
||||
.btn:hover{background:#30363d}
|
||||
.btn.s{border-color:var(--green);color:var(--green)}
|
||||
.btn.x{border-color:var(--red);color:var(--red)}
|
||||
.btn.r{border-color:var(--yellow);color:var(--yellow)}
|
||||
.btn.l{border-color:var(--accent);color:var(--accent)}
|
||||
.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 .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}
|
||||
.lc{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);max-height:300px;overflow-y:auto;background:#06080c;border:1px solid var(--border);border-radius:6px;padding:10px;white-space:pre-wrap}
|
||||
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:13px;z-index:999;opacity:0;pointer-events:none}
|
||||
.toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff}
|
||||
.ps{margin-top:24px}.ps h2{font-size:16px;color:var(--dim);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
.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}
|
||||
.mono{font-family:'Cascadia Code',Consolas,monospace;font-size: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.green{background:#3fb95020;color:var(--green)}
|
||||
.tag.red{background:#f8514920;color:var(--red)}
|
||||
.tag.yellow{background:#d2992220;color:var(--yellow)}
|
||||
.tag.dim{background:#8b949e20;color:var(--dim)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
|
||||
.dot.green{background:var(--green)}.dot.red{background:var(--red)}
|
||||
.dot.yellow{background:var(--yellow)}.dot.dim{background:var(--dim)}
|
||||
.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)}
|
||||
th{color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase}
|
||||
.log-line{font:11px/1.6 'Cascadia Code',Consolas,monospace;color:var(--dim);padding:1px 0}
|
||||
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)}
|
||||
.status-row{display:flex;align-items:center;gap:10px;padding:5px 0}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
@media(max-width:768px){.grid2{grid-template-columns:1fr}}
|
||||
.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)}
|
||||
@media(max-width:768px){.g2{grid-template-columns:1fr}}
|
||||
</style></head><body>
|
||||
<h1>AgentsMeeting</h1>
|
||||
<div class="subtitle" id="subtitle">监控驱动开发 Dashboard</div>
|
||||
<div class="tab-bar" id="tab-bar"></div>
|
||||
<div id="tab-contents"></div>
|
||||
<div class="subtitle" id="subtitle">Dashboard</div>
|
||||
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
|
||||
<script>
|
||||
const MAIN_TABS=[
|
||||
{id:'overview',label:'Overview'},
|
||||
{id:'kanban',label:'Kanban'},
|
||||
{id:'infra',label:'Infrastructure'},
|
||||
{id:'principles',label:'开发原则',children:[
|
||||
{id:'git',label:'A 源码管理'},
|
||||
{id:'services',label:'B 服务架构'},
|
||||
{id:'monitor',label:'C 监控结果'},
|
||||
{id:'todos',label:'D 自修复'},
|
||||
{id:'metagrowth',label:'E 元成长'},
|
||||
{id:'expected',label:'F 期望状态'},
|
||||
]},
|
||||
];
|
||||
let activeTab=localStorage.getItem('dashboardTab')||'overview';
|
||||
let activeSub=localStorage.getItem('dashboardSub')||'git';
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
function badge(id,t,c){const e=document.getElementById('badge-'+id);if(e){e.textContent=t;e.className='badge '+(c||'')}}
|
||||
function fill(id,h){document.getElementById('content-'+id).innerHTML=h;}
|
||||
function initTabs(){
|
||||
let bar='',ct='';
|
||||
MAIN_TABS.forEach(t=>{
|
||||
const isActive=t.id===activeTab||(t.children&&t.children.some(c=>c.id===activeTab));
|
||||
if(t.id==='principles'&&t.children&&t.children.some(c=>c.id===activeTab))activeTab='principles';
|
||||
bar+='<button class="tab-btn'+(isActive?' active':'')+'" data-tab="'+t.id+'" onclick="switchTab(\''+t.id+'\')">'+esc(t.label)+'<span class="badge" id="badge-'+t.id+'"></span></button>';
|
||||
if(t.children){
|
||||
ct+='<div class="tab-panel'+(isActive?' active':'')+'" id="panel-'+t.id+'">';
|
||||
ct+='<div class="sub-bar" id="sub-'+t.id+'">';
|
||||
t.children.forEach(c=>{ct+='<button class="sub-btn'+(c.id===activeSub?' active':'')+'" data-sub="'+c.id+'" onclick="switchSub(\''+c.id+'\')">'+esc(c.label)+'<span class="badge" id="badge-'+c.id+'"></span></button>';});
|
||||
ct+='</div><div id="content-'+t.id+'">';
|
||||
t.children.forEach(c=>{ct+='<div id="subpanel-'+c.id+'"'+(c.id===activeSub?'':' style="display:none"')+'><div class="section"><div id="content-'+c.id+'"><span class="dim">Loading...</span></div></div></div>';});
|
||||
ct+='</div></div>';
|
||||
}else{
|
||||
ct+='<div class="tab-panel'+(isActive?' active':'')+'" id="panel-'+t.id+'"><div class="section"><div id="content-'+t.id+'"><span class="dim">Loading...</span></div></div></div>';
|
||||
}
|
||||
});
|
||||
document.getElementById('tab-bar').innerHTML=bar;
|
||||
document.getElementById('tab-contents').innerHTML=ct;
|
||||
}
|
||||
function switchTab(id){
|
||||
if(id==='principles'){activeSub=localStorage.getItem('dashboardSub')||'git';}else{activeTab=id;localStorage.setItem('dashboardTab',id);}
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===id||(id==='principles'&&b.dataset.tab==='principles')));
|
||||
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.toggle('active',p.id==='panel-'+id));
|
||||
document.querySelectorAll('.sub-btn').forEach(b=>b.classList.toggle('active',b.dataset.sub===activeSub));
|
||||
document.querySelectorAll('[id^="subpanel-"]').forEach(p=>p.style.display=p.id==='subpanel-'+activeSub?'':'none');
|
||||
}
|
||||
function switchSub(id){
|
||||
activeSub=id;localStorage.setItem('dashboardSub',id);
|
||||
document.querySelectorAll('.sub-btn').forEach(b=>b.classList.toggle('active',b.dataset.sub===id));
|
||||
document.querySelectorAll('[id^="subpanel-"]').forEach(p=>p.style.display=p.id==='subpanel-'+id?'':'none');
|
||||
FETCHES[id]();
|
||||
}
|
||||
// ─── Fetch functions ───
|
||||
async function fetchOverview(){
|
||||
try{const[s,e,m]=await Promise.all([fetch('/api/services').then(r=>r.json()),fetch('/api/expected').then(r=>r.json()),fetch('/api/monitor').then(r=>r.json())]);
|
||||
const sc=s.services.filter(x=>x.health&&x.health.ok).length;fill('overview','<div class="grid2"><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px;text-align:center"><div style="font-size:32px;font-weight:700;color:var(--green)">'+sc+'/'+s.services.length+'</div><div style="color:var(--dim);font-size:12px">Services</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px;text-align:center"><div style="font-size:32px;font-weight:700;color:var(--green)">OK</div><div style="color:var(--dim);font-size:12px">System</div></div></div>');
|
||||
}catch(e){fill('overview','<span class="tag red">Error</span>');}}
|
||||
async function fetchGit(){try{const r=await fetch('/api/git'),d=await r.json();badge('git',d.log.length+' commits',d.dirty?'yellow':'green');
|
||||
let h=d.log.map(l=>{const 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 red">Error</span>');}}
|
||||
async function fetchServices(){try{const r=await fetch('/api/services'),d=await r.json(),ok=d.services.filter(s=>s.health&&s.health.ok).length;badge('services',ok+'/'+d.total,ok===d.total?'green':'red');
|
||||
let 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(s=>{const a=s.health&&s.health.ok;h+='<tr><td><span class="dot '+(a?'green':'red')+'"></span>'+esc(s.name)+'</td><td>'+(s.port||'-')+'</td><td><span class="tag '+(a?'green':'red')+'">'+(s.health?s.health.status:'?')+'</span></td><td>'+(s.watchdog?'<span class="dot green"></span>yes':'<span class="dot red"></span>no')+'</td><td><span class="tag dim">'+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('services',h);}catch(e){fill('services','<span class="tag red">Error</span>');}}
|
||||
async function fetchMonitor(){try{const r=await fetch('/api/monitor'),d=await r.json();let f=0,h='';
|
||||
if(d.tier1){const 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?'red':'green')+'">'+s.ok+'/'+s.total+' ok</span><span class="mono" style="color:var(--dim);margin-left:8px">'+esc(d.tier1.time)+'</span></div>';}
|
||||
if(d.tier2){const 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?'red':'green')+'">'+s.ok+'/'+s.total+' ok</span><span class="mono" style="color:var(--dim);margin-left:8px">'+esc(d.tier2.time)+'</span></div>';}
|
||||
h+='<table><tr><th>Task</th><th>Status</th></tr>';d.tasks.forEach(t=>{const o=t.status==='ok';if(!o)f=1;h+='<tr><td>'+esc(t.name)+'</td><td><span class="tag '+(o?'green':'red')+'">'+esc(t.status)+'</span></td></tr>';});h+='</table>';badge('principles',f?'issues':'ok',f?'red':'green');badge('monitor',f?'issues':'ok',f?'red':'green');fill('monitor',h);}catch(e){fill('monitor','<span class="tag red">Error</span>');}}
|
||||
async function fetchTodos(){try{const r=await fetch('/api/todos'),d=await r.json(),p=d.todos.filter(t=>t.status==='pending').length;badge('todos',d.count+' tot',p>0?'yellow':'green');
|
||||
let 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(t=>{const st=t.status==='completed'?'<span class="tag green">done</span>':t.status==='failed'?'<span class="tag red">fail</span>':'<span class="tag yellow">'+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);font-size:11px;background:var(--bg);padding:8px;border-radius:6px">'+d.executor_log_tail.split('\n').map(l=>'<div>'+esc(l)+'</div>').join('')+'</div>';}
|
||||
fill('todos',h);}catch(e){fill('todos','<span class="tag red">Error</span>');}}
|
||||
async function fetchMetagrowth(){try{const r=await fetch('/api/metagrowth'),d=await r.json();
|
||||
badge('metagrowth',d.status==='active'?'active':'planned',d.status==='active'?'green':'yellow');
|
||||
let h='<div style="margin-bottom:10px"><strong>E — 元成长回路</strong> <span class="tag '+(d.meta_growth_enabled?'green':'yellow')+'">'+(d.meta_growth_enabled?'active':'planned Phase 3')+'</span></div>';
|
||||
h+='<div class="grid2">';
|
||||
h+='<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+'</div><div style="color:var(--dim);font-size:11px">Total Fixes</div></div>';
|
||||
h+='<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+'</div><div style="color:var(--dim);font-size:11px">Completed Fixes</div></div>';
|
||||
h+='<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+'</div><div style="color:var(--dim);font-size:11px">Git Commits</div></div>';
|
||||
h+='<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>';
|
||||
h+='</div><div style="margin-top:10px;color:var(--dim);font-size:12px">'+esc(d.description||'')+'</div>';
|
||||
fill('metagrowth',h);}catch(e){fill('metagrowth','<span class="tag red">Error</span>');badge('metagrowth','err','red');}}
|
||||
async function fetchExpected(){try{const r=await fetch('/api/expected'),d=await r.json();let ok=0,fl=0;
|
||||
d.expected.forEach(e=>{const a=d.actual[e.name]||'unknown',cmp=(e.expected.startsWith('running')&&a==='running')||(e.expected.startsWith('scheduled')&&(a==='scheduled'||a==='running'));if(cmp)ok++;else fl++;});
|
||||
badge('expected',ok+'/'+(ok+fl),fl>0?'red':'green');
|
||||
let h='<table><tr><th>Component</th><th>Expected</th><th>Actual</th><th>Status</th></tr>';
|
||||
d.expected.forEach(e=>{const a=d.actual[e.name]||'unknown',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="dot '+(cmp?'green':'red')+'"></span>'+esc(a)+'</td><td>'+(cmp?'<span class="tag green">OK</span>':'<span class="tag red">FAIL</span>')+'</td></tr>';});
|
||||
h+='</table>';fill('expected',h);}catch(e){fill('expected','<span class="tag red">Error</span>');}}
|
||||
async function fetchKanban(){try{const r=await fetch('/api/kanban'),d=await r.json();badge('kanban',(d.tasks||[]).length+' tasks','dim');if(!d.tasks||!d.tasks.length){fill('kanban','<span class="dim">No tasks</span>');return;}
|
||||
const c={ready:'var(--yellow)',progress:'var(--accent)',done:'var(--green)',blocked:'var(--red)'};fill('kanban',d.tasks.slice(0,30).map(t=>'<div class="status-row"><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 dim">'+esc(t.status)+'</span>'+(t.assignee?'<span class="tag">'+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 red">Error</span>');}}
|
||||
async function fetchInfra(){try{const r=await fetch('/api/ejabberd'),d=await r.json(),a=d.alive||d.xmpp_bot_connected;fill('infra','<div class="status-row"><span class="dot '+(a?'green':'red')+'"></span><strong>Ejabberd XMPP</strong><span class="tag '+(a?'green':'red')+'">'+(a?'ALIVE':'DOWN')+'</span><span style="color:var(--dim);margin-left:auto">'+(d.online_jids||[]).join(', ')+'</span></div>');badge('infra',a?'alive':'down',a?'green':'red');}catch(e){fill('infra','<span class="tag red">Error</span>');badge('infra','error','red');}}
|
||||
|
||||
initTabs();
|
||||
const FETCHES={overview:fetchOverview,git:fetchGit,services:fetchServices,monitor:fetchMonitor,todos:fetchTodos,metagrowth:fetchMetagrowth,expected:fetchExpected,kanban:fetchKanban,infra:fetchInfra};
|
||||
async function loadAll(){document.getElementById('subtitle').textContent=new Date().toLocaleString('zh-CN',{timeZone:'Asia/Shanghai'});await Promise.all(Object.values(FETCHES).map(f=>f()));}
|
||||
loadAll();setInterval(loadAll,15000);setInterval(fetchKanban,20000);
|
||||
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,">")}
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user