A-F父Tab + E元成长API + 界面重排
- 结构: Overview / Kanban / Infra / 开发原则(A-F子Tab) - E: 新增/api/metagrowth路由 + dashboard展示 - 排序: 开发原则排在最后 - 前端: 父Tab切换子Tab, localStorage记忆
This commit is contained in:
@@ -661,6 +661,49 @@ def api_monitor():
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# E — 元成长回路可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@app.route("/api/metagrowth")
|
||||
def api_metagrowth():
|
||||
"""元成长回路(Phase 3 规划中)"""
|
||||
try:
|
||||
# 统计已完成的修复次数
|
||||
todo_file = TEMP_DIR / "health_todos.jsonl"
|
||||
total_fixes = 0
|
||||
completed_fixes = 0
|
||||
if todo_file.exists():
|
||||
with open(str(todo_file)) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
total_fixes += 1
|
||||
entry = json.loads(line)
|
||||
if entry.get("status") == "completed":
|
||||
completed_fixes += 1
|
||||
# 统计 git 提交次数(用作开发活动指标)
|
||||
git_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
||||
commit_count = 0
|
||||
try:
|
||||
r = subprocess.run(["git", "rev-list", "--count", "HEAD"],
|
||||
cwd=git_dir, capture_output=True, text=True, timeout=5)
|
||||
if r.returncode == 0:
|
||||
commit_count = int(r.stdout.strip())
|
||||
except:
|
||||
pass
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"status": "planned", # Phase 3
|
||||
"total_fixes": total_fixes,
|
||||
"completed_fixes": completed_fixes,
|
||||
"commit_count": commit_count,
|
||||
"meta_growth_enabled": False,
|
||||
"description": "E 元成长回路是 Phase 3 范围。启用后将自动分析修复模式并扩展检查规则。",
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)})
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# D — 自修复流水线可视化
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -3,21 +3,29 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AgentsMeeting — 监控驱动开发 Dashboard</title>
|
||||
<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}
|
||||
: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:16px;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}
|
||||
.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}
|
||||
@@ -28,10 +36,8 @@ h1{font-size:22px;font-weight:600;color:var(--accent);margin-bottom:2px}
|
||||
.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)}
|
||||
.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}
|
||||
@@ -46,59 +52,97 @@ th{color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase}
|
||||
<div class="tab-bar" id="tab-bar"></div>
|
||||
<div id="tab-contents"></div>
|
||||
<script>
|
||||
const TABS=[
|
||||
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 期望状态'},
|
||||
{id:'kanban',label:'Kanban'},
|
||||
{id:'infra',label:'Infrastructure'},
|
||||
]},
|
||||
];
|
||||
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 initTabs(){
|
||||
const bar=document.getElementById('tab-bar');
|
||||
bar.innerHTML=TABS.map(t=>'<button class="tab-btn'+(t.id===activeTab?' active':'')+'" data-tab="'+t.id+'" onclick="switchTab(\''+t.id+'\')">'+esc(t.label)+'<span class="badge" id="badge-'+t.id+'"></span></button>').join('');
|
||||
document.getElementById('tab-contents').innerHTML=TABS.map(t=>'<div class="tab-panel'+(t.id===activeTab?' active':'')+'" id="panel-'+t.id+'"><div class="section"><div id="content-'+t.id+'"><span class="dim">Loading...</span></div></div></div>').join('');
|
||||
}
|
||||
function switchTab(id){
|
||||
activeTab=id;localStorage.setItem('dashboardTab',id);
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===id));
|
||||
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.toggle('active',p.id==='panel-'+id));
|
||||
}
|
||||
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,ec=e.expected.filter(x=>{const a=e.actual[x.name]||'';return(x.expected.startsWith('running')&&a==='running')||(x.expected.startsWith('scheduled')&&(a==='scheduled'||a==='running'));}).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 Healthy</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:'+(sc===s.services.length?'var(--green)':'var(--yellow)')+'">'+(sc===s.services.length?'OK':'⚠')+'</div><div style="color:var(--dim);font-size:12px">Expected '+(sc===s.services.length?'Compliant':'Issues')+'</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)">'+ec+'/'+e.expected.length+'</div><div style="color:var(--dim);font-size:12px">Expected Compliant</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)">ONLINE</div><div style="color:var(--dim);font-size:12px">Ejabberd XMPP</div></div></div>');
|
||||
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',ok===d.total?'green':'red');
|
||||
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('monitor',f?'issues':'all 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+' total',p>0?'yellow':'green');
|
||||
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)+' ok',fl>0?'red':'green');
|
||||
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,expected:fetchExpected,kanban:fetchKanban,infra:fetchInfra};
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user