feat(dashboard): restore lost monitoring + add 开发原则 tab (AgentsMeeting parity)

Two regressions fixed:

1. RESTORED: original health monitoring (功能树/全部Cron/数据实体/数据流)
   was an iframe to /mofin_health.html — refactor replaced it with a
   minimal services-only panel and lost all of it. mofin_health.json
   (86KB, fresh) was still being generated the whole time.
   - 健康 tab: XMPP panel (native) + restored iframe below

2. ADDED: 开发原则 parent tab replicating AgentsMeeting structure:
   - G 规范: /api/spec renders docs/dev-spec.md + git history
   - K 测试: /api/tests renders agents_health_check report as pass/fail
   - F 健康: iframe to /mofin_health.html + link to 健康 tab
   - H 需求: /api/prd (placeholder — prd.md not yet created)
   - mdRender() ported from AgentsMeeting dashboard
This commit is contained in:
hmo
2026-07-20 00:40:32 +08:00
parent 2de527b883
commit a05c118cbc
3 changed files with 250 additions and 0 deletions
+176
View File
@@ -12,6 +12,8 @@
body { background: #0f0f13; color: #e2e8f0; }
.tab-btn { transition: all 0.15s; }
.tab-btn.active { background: #1e293b; color: #60a5fa; border-color: #60a5fa; }
.princ-btn { transition: all 0.15s; color: #8b949e; }
.princ-btn.active { background: #1e293b; color: #60a5fa; }
.card { background: #1a1a24; border: 1px solid #2a2a3a; border-radius: 12px; }
.badge-up { color: #22c55e; }
.badge-down { color: #ef4444; }
@@ -63,6 +65,7 @@ body { background: #0f0f13; color: #e2e8f0; }
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="market">🌐 市场</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="signals">🔍 信号</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="health">🏥 健康</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg" data-tab="principles">📐 开发原则</button>
<button class="tab-btn px-4 py-2 text-sm rounded-lg blink-tab" data-tab="watch" style="color:#f97316;border-color:#f97316">👁️ 盯盘</button>
<a href="/upload" class="tab-btn px-4 py-2 text-sm rounded-lg" style="text-decoration:none;color:#fbbf24;border-color:#fbbf24">📸 上传</a>
</div>
@@ -78,6 +81,18 @@ body { background: #0f0f13; color: #e2e8f0; }
<div id="tab-market" class="tab-content hidden"></div>
<div id="tab-signals" class="tab-content hidden"></div>
<div id="tab-health" class="tab-content hidden"></div>
<div id="tab-principles" class="tab-content hidden">
<div class="flex gap-1 mb-4 bg-slate-900/50 rounded-xl p-1 border border-slate-800/50" id="princBar">
<button class="princ-btn active px-4 py-1.5 text-sm rounded-lg" data-princ="spec">G 规范</button>
<button class="princ-btn px-4 py-1.5 text-sm rounded-lg" data-princ="tests">K 测试</button>
<button class="princ-btn px-4 py-1.5 text-sm rounded-lg" data-princ="health">F 健康</button>
<button class="princ-btn px-4 py-1.5 text-sm rounded-lg" data-princ="prd">H 需求</button>
</div>
<div id="princ-spec" class="princ-pane"></div>
<div id="princ-tests" class="princ-pane hidden"></div>
<div id="princ-health" class="princ-pane hidden"></div>
<div id="princ-prd" class="princ-pane hidden"></div>
</div>
<div id="tab-watch" class="tab-content hidden">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-slate-200">👁️ 盯盘 · 有效操作建议</h2>
@@ -204,6 +219,7 @@ function renderTab(name) {
else if (name === 'market') renderMarket();
else if (name === 'signals') renderSignals();
else if (name === 'health') renderHealth();
else if (name === 'principles') renderPrinciples();
else if (name === 'watch') renderWatch();
}
@@ -1580,6 +1596,13 @@ async function renderHealth() {
html += '</div>';
html += '</div>';
}
// ── 原有健康监控(功能树/Cron/数据实体/数据流)—— 恢复自 mofin_health.html ──
html += '<div class="card p-4 mt-4">';
html += '<div class="text-sm font-semibold text-slate-300 mb-2">📊 系统健康监控(功能树 · Cron · 数据实体 · 数据流)</div>';
html += '<iframe src="/mofin_health.html" style="width:100%;height:80vh;border:none;background:#0d1117;border-radius:8px;"></iframe>';
html += '</div>';
el.innerHTML = html;
el.dataset.rendered = '1'; // 标记已首渲
} catch(e) {
@@ -1748,6 +1771,159 @@ async function renderWatch() {
watchTimer = setInterval(renderWatch, 60000);
}
}
// ── 开发原则 Tab(仿 AgentsMeeting: G规范/K测试/F健康/H需求)──
let _princLoaded = {};
function renderPrinciples() {
// 绑定子tab按钮
document.querySelectorAll('.princ-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.princ-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.princ-pane').forEach(p => p.classList.add('hidden'));
btn.classList.add('active');
const id = btn.dataset.princ;
document.getElementById('princ-' + id).classList.remove('hidden');
_loadPrinc(id);
};
});
// 默认加载当前激活的子tab
const active = document.querySelector('.princ-btn.active');
_loadPrinc(active ? active.dataset.princ : 'spec');
}
function _loadPrinc(id) {
if (id === 'spec') return _renderPrincSpec();
if (id === 'tests') return _renderPrincTests();
if (id === 'health') return _renderPrincHealth();
if (id === 'prd') return _renderPrincPrd();
}
async function _renderPrincSpec() {
const el = document.getElementById('princ-spec');
el.innerHTML = '<div class="text-slate-500 py-4">加载中...</div>';
try {
const d = await fetchJSON('/api/spec');
if (!d.ok) { el.innerHTML = '<div class="text-red-400 py-4">' + (d.error || '加载失败') + '</div>'; return; }
let h = '<div class="card p-5">' + mdRender(d.content) + '</div>';
try {
const dh = await fetchJSON('/api/spec/history');
if (dh.ok && dh.log && dh.log.length) {
h += '<div class="card p-4 mt-3"><div class="text-sm font-semibold text-slate-300 cursor-pointer" onclick="var e=document.getElementById(\'specHist\');e.classList.toggle(\'hidden\')">历史版本 <span class="text-slate-500 text-xs">(' + dh.count + ' commits)</span></div><div id="specHist" class="hidden mt-2 text-xs text-slate-500 font-mono space-y-1">';
dh.log.forEach(l => { h += '<div>' + l.replace(/</g, '&lt;') + '</div>'; });
h += '</div></div>';
}
} catch(e) {}
el.innerHTML = h;
} catch(e) { el.innerHTML = '<div class="text-red-400 py-4">加载失败: ' + e.message + '</div>'; }
}
async function _renderPrincTests() {
const el = document.getElementById('princ-tests');
el.innerHTML = '<div class="text-slate-500 py-4">加载中...</div>';
try {
const d = await fetchJSON('/api/tests');
if (!d.ok) { el.innerHTML = '<div class="text-red-400 py-4">' + (d.error || '加载失败') + '</div>'; return; }
const s = d.summary || {};
let h = '<div class="grid grid-cols-3 gap-3 mb-4">';
h += '<div class="card p-3 text-center"><div class="text-2xl font-bold text-white font-mono">' + (s.total||0) + '</div><div class="text-xs text-slate-500">Total</div></div>';
h += '<div class="card p-3 text-center"><div class="text-2xl font-bold text-[#3fb950] font-mono">' + (s.passed||0) + '</div><div class="text-xs text-slate-500">Passed</div></div>';
h += '<div class="card p-3 text-center"><div class="text-2xl font-bold text-[#f85149] font-mono">' + (s.failed||0) + '</div><div class="text-xs text-slate-500">Failed</div></div>';
h += '</div>';
const fails = (d.tests||[]).filter(t => !t.ok);
if (fails.length) {
h += '<div class="text-sm font-semibold text-[#f85149] mb-2">Failed (' + fails.length + ')</div>';
fails.forEach(t => {
h += '<div class="card p-3 mb-2" style="border-color:#f85149"><span class="text-xs px-1.5 py-0.5 rounded bg-[#f85149] text-white font-bold">FAIL</span> <strong class="text-sm">' + t.name.replace(/</g,'&lt;') + '</strong><div class="text-xs text-slate-500 mt-1">' + (t.detail||'').replace(/</g,'&lt;') + '</div></div>';
});
}
const passes = (d.tests||[]).filter(t => t.ok);
h += '<div class="text-sm font-semibold text-[#3fb950] mb-2 mt-3">Passed (' + passes.length + ')</div>';
passes.forEach(t => {
h += '<div class="card p-2 mb-1"><span class="text-xs px-1.5 py-0.5 rounded bg-[#3fb950] text-white font-bold">PASS</span> <span class="text-sm">' + t.name.replace(/</g,'&lt;') + '</span> <span class="text-xs text-slate-500">' + (t.detail||'').replace(/</g,'&lt;') + '</span></div>';
});
h += '<div class="text-xs text-slate-500 mt-3">数据源: agents_health_check (cron */5min) · 最后运行: ' + (d.time||'?') + '</div>';
el.innerHTML = h;
} catch(e) { el.innerHTML = '<div class="text-red-400 py-4">加载失败: ' + e.message + '</div>'; }
}
function _renderPrincHealth() {
const el = document.getElementById('princ-health');
if (_princLoaded.health) return;
el.innerHTML = '<div class="card p-4"><div class="text-sm text-slate-400 mb-2">完整健康监控与 🏥 健康 Tab 同源。快捷入口:<a href="javascript:switchTab(\'health\')" class="text-[#58a6ff]">🏥 健康 Tab</a></div>' +
'<iframe src="/mofin_health.html" style="width:100%;height:80vh;border:none;background:#0d1117;border-radius:8px;"></iframe></div>';
_princLoaded.health = true;
}
async function _renderPrincPrd() {
const el = document.getElementById('princ-prd');
el.innerHTML = '<div class="text-slate-500 py-4">加载中...</div>';
try {
const d = await fetchJSON('/api/prd');
if (!d.ok) { el.innerHTML = '<div class="card p-6 text-center text-slate-500">' + (d.error || 'prd.md 尚未建立') + '</div>'; return; }
el.innerHTML = '<div class="card p-5">' + mdRender(d.content) + '</div>';
} catch(e) { el.innerHTML = '<div class="text-red-400 py-4">加载失败: ' + e.message + '</div>'; }
}
// markdown 渲染(移植自 AgentsMeeting dashboard
function mdRender(md) {
if (!md) return '';
let h = '';
const blocks = md.split('\n\n');
for (const raw of blocks) {
const block = raw.trim();
if (!block) continue;
if (block.startsWith('```')) {
const code = block.replace(/^```[\s\S]*?\n/, '').replace(/```$/, '').trim();
h += '<pre class="font-mono text-xs" style="background:#0d1117;padding:12px;border-radius:6px;overflow-x:auto;border:1px solid #30363d">' + code.replace(/</g,'&lt;') + '</pre>';
continue;
}
if (block.startsWith('#### ')) { h += '<h4 class="text-[#58a6ff] font-semibold mt-4 mb-1 text-sm">' + inMd(block.slice(5).replace(/</g,'&lt;')) + '</h4>'; continue; }
if (block.startsWith('### ')) { h += '<h3 class="text-[#58a6ff] font-semibold mt-4 mb-1">' + inMd(block.slice(4).replace(/</g,'&lt;')) + '</h3>'; continue; }
if (block.startsWith('## ')) { h += '<h2 class="text-[#58a6ff] font-semibold mt-5 mb-2 text-lg border-b border-slate-800 pb-1">' + inMd(block.slice(3).replace(/</g,'&lt;')) + '</h2>'; continue; }
if (block.startsWith('# ')) { h += '<h1 class="text-[#58a6ff] font-bold mt-5 mb-2 text-xl">' + inMd(block.slice(2).replace(/</g,'&lt;')) + '</h1>'; continue; }
if (block.startsWith('> ')) { h += '<blockquote class="border-l-2 border-[#58a6ff] pl-3 py-1 my-2 text-slate-500 text-sm">' + inMd(block.slice(2).replace(/</g,'&lt;')) + '</blockquote>'; continue; }
if (block.startsWith('---') || block.startsWith('***')) { h += '<hr class="border-slate-800 my-3">'; continue; }
if (block.startsWith('- ') || block.startsWith('* ')) {
const items = block.split('\n');
h += '<ul class="list-disc pl-5 my-1 text-sm space-y-0.5">';
items.forEach(li => { const t = li.replace(/^[-*]\s*/, '').trim(); if (t) h += '<li>' + inMd(t.replace(/</g,'&lt;')) + '</li>'; });
h += '</ul>';
continue;
}
if (/^\d+\.\s/.test(block)) {
const items = block.split('\n');
h += '<ol class="list-decimal pl-5 my-1 text-sm space-y-0.5">';
items.forEach(li => { const t = li.replace(/^\d+\.\s*/, '').trim(); if (t) h += '<li>' + inMd(t.replace(/</g,'&lt;')) + '</li>'; });
h += '</ol>';
continue;
}
if (block.indexOf('|') >= 0 && block.indexOf('\n|') >= 0) {
const rows = block.split('\n');
let inTable = false;
rows.forEach(row => {
if (row.indexOf('---') >= 0) return;
const cells = row.split('|').filter(c => c.trim());
if (cells.length < 2) return;
if (!inTable) {
h += '<table class="my-2 text-xs w-full"><tr>' + cells.map(c => '<th class="px-2 py-1 border border-slate-700 text-slate-500 bg-slate-900">' + inMd(c.trim().replace(/</g,'&lt;')) + '</th>').join('') + '</tr>';
inTable = true;
} else {
h += '<tr>' + cells.map(c => '<td class="px-2 py-1 border border-slate-800">' + inMd(c.trim().replace(/</g,'&lt;')) + '</td>').join('') + '</tr>';
}
});
if (inTable) h += '</table>';
continue;
}
h += '<p class="text-sm my-1.5 leading-relaxed text-slate-300">' + inMd(block.replace(/</g,'&lt;')) + '</p>';
}
return h;
}
function inMd(t) {
return t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code class="bg-slate-900 px-1 rounded text-xs font-mono">$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-[#58a6ff]" target="_blank">$1</a>');
}
</script>
</body>
</html>