Tab布局 + 中文locale修复 + false positive修复
- Tab导航: A-F各一个Tab,body自动保存当前Tab - 中文locale: schtasks输出'就绪'正确识别为scheduled - 假阳性修复: watchdog检查兼容中文系统 - HTML精简至4KB内联,所有API 200
This commit is contained in:
@@ -1,258 +1,104 @@
|
||||
<!DOCTYPE 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>
|
||||
: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:20px; }
|
||||
h1 { font-size:22px; font-weight:600; color:var(--accent); margin-bottom:2px; }
|
||||
.subtitle { color:var(--dim); font-size:12px; margin-bottom:16px; }
|
||||
.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; margin-bottom:10px; color:var(--accent); display:flex; align-items:center; gap:8px; }
|
||||
.section h2 .badge { font-size:10px; padding:1px 8px; border-radius:8px; background:var(--bg); font-weight:400; }
|
||||
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
||||
.grid3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px; }
|
||||
.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); }
|
||||
.log-line .dirty { color:var(--red); }
|
||||
.tree-item { display:flex; align-items:center; gap:8px; padding:4px 0; }
|
||||
.tree-indent { padding-left:24px; }
|
||||
.status-row { display:flex; align-items:center; gap:10px; padding:5px 0; }
|
||||
@media(max-width:768px){ .grid2,.grid3{ grid-template-columns:1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>AgentsMeeting — 监控驱动开发</h1>
|
||||
<div class="subtitle" id="subtitle">加载中...</div>
|
||||
|
||||
<!-- A: 源码管理 -->
|
||||
<div class="section" id="sec-a">
|
||||
<h2>A — 源码管理 <span class="badge" id="git-badge"></span></h2>
|
||||
<div id="git-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- B: 服务架构 -->
|
||||
<div class="section" id="sec-b">
|
||||
<h2>B — 服务架构 <span class="badge" id="svc-badge"></span></h2>
|
||||
<div id="svc-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- C: 监控结果 -->
|
||||
<div class="section" id="sec-c">
|
||||
<h2>C — 监控结果 <span class="badge" id="mon-badge"></span></h2>
|
||||
<div id="mon-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- D: 自修复 -->
|
||||
<div class="section" id="sec-d">
|
||||
<h2>D — 自修复流水线 <span class="badge" id="todo-badge"></span></h2>
|
||||
<div id="todo-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- F: 期望状态 -->
|
||||
<div class="section" id="sec-f">
|
||||
<h2>F — 期望状态 vs 实际 <span class="badge" id="exp-badge"></span></h2>
|
||||
<div id="exp-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Kanban -->
|
||||
<div class="section">
|
||||
<h2>Kanban <span class="badge" id="kanban-count"></span></h2>
|
||||
<div id="kanban-list"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Infrastructure -->
|
||||
<div class="section">
|
||||
<h2>Infrastructure</h2>
|
||||
<div id="infra-content"><span class="dim">加载中...</span></div>
|
||||
</div>
|
||||
|
||||
: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: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-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}
|
||||
.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>
|
||||
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function clr(v) { return v ? 'green' : 'red'; }
|
||||
|
||||
// A: Git
|
||||
async function fetchGit() {
|
||||
try {
|
||||
const r = await fetch('/api/git'); const d = await r.json();
|
||||
const el = document.getElementById('git-content');
|
||||
document.getElementById('git-badge').textContent = d.dirty ? '⚠ dirty' : (d.log.length + ' commits');
|
||||
el.innerHTML = 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 (d.dirty) el.innerHTML += '<div class="log-line" style="color:var(--red);margin-top:4px;">⚠ 未提交的改动</div>';
|
||||
} catch(e) {}
|
||||
const TABS=[
|
||||
{id:'overview',label:'Overview'},
|
||||
{id:'git',label:'A 源码管理'},
|
||||
{id:'services',label:'B 服务架构'},
|
||||
{id:'monitor',label:'C 监控结果'},
|
||||
{id:'todos',label:'D 自修复'},
|
||||
{id:'expected',label:'F 期望状态'},
|
||||
{id:'kanban',label:'Kanban'},
|
||||
{id:'infra',label:'Infrastructure'},
|
||||
];
|
||||
let activeTab=localStorage.getItem('dashboardTab')||'overview';
|
||||
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('');
|
||||
}
|
||||
|
||||
// B: Services
|
||||
async function fetchServices() {
|
||||
try {
|
||||
const r = await fetch('/api/services'); const d = await r.json();
|
||||
const el = document.getElementById('svc-content');
|
||||
const ok = d.services.filter(s => s.health && s.health.ok).length;
|
||||
document.getElementById('svc-badge').textContent = ok + '/' + d.total + ' ok';
|
||||
el.innerHTML = '<table><tr><th>Service</th><th>Port</th><th>/health</th><th>Watchdog</th><th>Type</th><th>Depends</th></tr>'
|
||||
+ d.services.map(s => '<tr><td><span class="dot ' + clr(s.health&&s.health.ok) + '"></span>' + esc(s.name) + '</td>'
|
||||
+ '<td>' + (s.port||'-') + '</td>'
|
||||
+ '<td><span class="tag ' + (s.health&&s.health.ok?'green':'red') + '">' + (s.health&&s.health.status||'down') + '</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>').join('')
|
||||
+ '</table>'
|
||||
+ '<div style="margin-top:6px;color:var(--dim);font-size:11px;">Watchdog 覆盖: ' + d.watched + '/' + d.total + '</div>';
|
||||
} catch(e) {}
|
||||
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));
|
||||
}
|
||||
|
||||
// C: Monitor
|
||||
async function fetchMonitor() {
|
||||
try {
|
||||
const r = await fetch('/api/monitor'); const d = await r.json();
|
||||
const el = document.getElementById('mon-content');
|
||||
let html = '';
|
||||
if (d.tier1) {
|
||||
const s = d.tier1.summary || {};
|
||||
html += '<div style="margin-bottom:8px;"><strong>Tier 1 快速检查</strong> <span class="tag ' + (s.fail>0?'red':'green') + '">' + s.ok + '/' + s.total + ' ok, ' + s.fail + ' fail</span>'
|
||||
+ '<div class="mono" style="margin-top:4px;">检查时间: ' + esc(d.tier1.time) + '</div></div>';
|
||||
}
|
||||
if (d.tier2) {
|
||||
const s = d.tier2.summary || {};
|
||||
html += '<div style="margin-bottom:8px;"><strong>Tier 2 每日检查</strong> <span class="tag ' + (s.fail>0?'red':'green') + '">' + s.ok + '/' + s.total + ' ok</span>'
|
||||
+ '<div class="mono" style="margin-top:4px;">检查时间: ' + esc(d.tier2.time) + '</div>';
|
||||
if (d.tier2.recommendations && d.tier2.recommendations.length) {
|
||||
html += '<div style="margin-top:4px;color:var(--yellow);">' + d.tier2.recommendations.map(r => '<div>→ ' + esc(r) + '</div>').join('') + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<table><tr><th>Task</th><th>Status</th></tr>';
|
||||
d.tasks.forEach(t => {
|
||||
const st = t.status === 'ok' ? '<span class="tag green">Ready</span>' : '<span class="tag red">' + esc(t.status) + '</span>';
|
||||
html += '<tr><td>' + esc(t.name) + '</td><td>' + st + '</td></tr>';
|
||||
});
|
||||
html += '</table>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// D: TODOs
|
||||
async function fetchTodos() {
|
||||
try {
|
||||
const r = await fetch('/api/todos'); const d = await r.json();
|
||||
const el = document.getElementById('todo-content');
|
||||
document.getElementById('todo-badge').textContent = d.count + ' records';
|
||||
let html = '';
|
||||
if (d.todos.length) {
|
||||
html += '<table><tr><th>Time</th><th>Service</th><th>Issue</th><th>Status</th><th>Result</th></tr>';
|
||||
d.todos.slice(-10).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>';
|
||||
html += '<tr><td class="mono">' + esc((t.created||'').substring(11,19)) + '</td>'
|
||||
+ '<td>' + esc(t.service) + '</td>'
|
||||
+ '<td>' + esc((t.issue||'').substring(0,40)) + '</td>'
|
||||
+ '<td>' + st + '</td>'
|
||||
+ '<td class="mono">' + esc((t.result||'').substring(0,30)) + '</td></tr>';
|
||||
});
|
||||
html += '</table>';
|
||||
} else {
|
||||
html += '<span style="color:var(--dim);">暂无 TODO 记录</span>';
|
||||
}
|
||||
if (d.executor_log_tail) {
|
||||
html += '<div style="margin-top:8px;"><strong>执行器日志</strong></div><div class="mono" style="color:var(--dim);font-size:11px;">';
|
||||
html += d.executor_log_tail.split('\n').map(l => '<div>' + esc(l) + '</div>').join('');
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// F: Expected vs Actual
|
||||
async function fetchExpected() {
|
||||
try {
|
||||
const r = await fetch('/api/expected'); const d = await r.json();
|
||||
const el = document.getElementById('exp-content');
|
||||
let ok = 0, fail = 0;
|
||||
d.expected.forEach(e => {
|
||||
const a = d.actual[e.name] || 'unknown';
|
||||
if (e.expected.startsWith('running') && a === 'running') ok++;
|
||||
else if (e.expected.startsWith('scheduled') && a === 'scheduled') ok++;
|
||||
else fail++;
|
||||
});
|
||||
document.getElementById('exp-badge').textContent = ok + '/' + (ok+fail) + ' 合规';
|
||||
let html = '<table><tr><th>Component</th><th>期望</th><th>实际</th><th>合规</th></tr>';
|
||||
d.expected.forEach(e => {
|
||||
const a = d.actual[e.name] || 'unknown';
|
||||
const compliant = (e.expected.startsWith('running') && a === 'running') || (e.expected.startsWith('scheduled') && a === 'scheduled');
|
||||
const dot = compliant ? 'green' : 'red';
|
||||
html += '<tr><td>' + (e.critical ? '<strong>' : '') + esc(e.name) + (e.critical ? '</strong>' : '') + '</td>'
|
||||
+ '<td>' + esc(e.expected) + '</td>'
|
||||
+ '<td><span class="dot ' + dot + '"></span>' + esc(a) + '</td>'
|
||||
+ '<td>' + (compliant ? '<span class="tag green">✅</span>' : '<span class="tag red">❌</span>') + '</td></tr>';
|
||||
});
|
||||
html += '</table>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// Kanban
|
||||
async function fetchKanban() {
|
||||
try {
|
||||
const r = await fetch('/api/kanban'); const d = await r.json();
|
||||
const el = document.getElementById('kanban-list');
|
||||
document.getElementById('kanban-count').textContent = d.tasks ? '(' + d.tasks.length + ')' : '';
|
||||
if (!d.tasks || !d.tasks.length) { el.innerHTML = '<span style="color:var(--dim);font-size:12px;">No tasks</span>'; return; }
|
||||
const colors = {ready:'var(--yellow)',progress:'var(--accent)',done:'var(--green)',blocked:'var(--red)'};
|
||||
el.innerHTML = d.tasks.slice(0,10).map(t =>
|
||||
'<div class="status-row"><span style="width:8px;height:8px;border-radius:50%;background:' + (colors[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) {}
|
||||
}
|
||||
|
||||
// Infrastructure
|
||||
async function fetchInfra() {
|
||||
try {
|
||||
const r = await fetch('/api/ejabberd'); const d = await r.json();
|
||||
const el = document.getElementById('infra-content');
|
||||
const alive = d.alive || d.xmpp_bot_connected;
|
||||
el.innerHTML = '<div class="status-row"><span class="dot ' + (alive?'green':'red') + '"></span>'
|
||||
+ '<strong>Ejabberd XMPP</strong> <span class="tag ' + (alive?'green':'red') + '">' + (alive?'ALIVE':'DOWN') + '</span>'
|
||||
+ '<span style="color:var(--dim);margin-left:auto;">在线: ' + (d.online_jids||[]).join(', ') + '</span></div>';
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// Init
|
||||
async function init() {
|
||||
const ts = new Date().toLocaleString('zh-CN', {timeZone:'Asia/Shanghai'});
|
||||
document.getElementById('subtitle').textContent = 'Last updated: ' + ts;
|
||||
await Promise.all([fetchGit(), fetchServices(), fetchMonitor(), fetchTodos(), fetchExpected(), fetchKanban(), fetchInfra()]);
|
||||
}
|
||||
|
||||
init();
|
||||
setInterval(init, 10000);
|
||||
setInterval(fetchKanban, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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;}
|
||||
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>');
|
||||
}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');
|
||||
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');
|
||||
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 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');
|
||||
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};
|
||||
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