Files
AgentsMeeting/gateway/scripts/templates/dashboard.html
T
hmo 69fa67e24f F可视化: dashboard 完整实现 A-F 六原则呈现 + 弹窗修复
- A: git log + dirty status
- B: 服务架构矩阵 (/health + watchdog覆盖 + 拓扑依赖)
- C: Tier1/Tier2 监控结果 + 定时任务状态
- D: TODO执行记录 + escalation日志
- F: 期望状态 vs 实际状态合规矩阵
- Kanban: dashboard内嵌版块
- 弹窗修复: 计划任务改用pythonw.exe + subprocess加CREATE_NO_WINDOW
- 修复ast_grep_replace导致的
2026-07-14 02:49:07 +08:00

259 lines
12 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>
: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>
<script>
function esc(s) { return String(s||'').replace(/&/g,'&amp;').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) {}
}
// 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) {}
}
// 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>