Files
AgentsMeeting/gateway/scripts/templates/dashboard.html
T
hmo f284ddfd3f A-F父Tab + E元成长API + 界面重排
- 结构: Overview / Kanban / Infra / 开发原则(A-F子Tab)
- E: 新增/api/metagrowth路由 + dashboard展示
- 排序: 开发原则排在最后
- 前端: 父Tab切换子Tab, localStorage记忆
2026-07-14 02:49:08 +08:00

149 lines
16 KiB
HTML

<!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}
.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}
.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)}
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}
.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}}
</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>
<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,'&amp;').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);
</script></body></html>