Files
MoFin/static/mofin_health.html
T

206 lines
9.5 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>MoFin 健康监控</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif}
body{background:#0d1117;color:#c9d1d9;padding:20px}
h1{font-size:22px;margin-bottom:16px;color:#58a6ff}
.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid #30363d;flex-wrap:wrap}
.tab{padding:8px 20px;cursor:pointer;border:1px solid transparent;border-radius:4px 4px 0 0;color:#8b949e;font-size:14px}
.tab.active{background:#161b22;border-color:#30363d #30363d #161b22;color:#c9d1d9}
.tab:hover{color:#58a6ff}
.panel{display:none}
.panel.active{display:block}
.badge{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px}
.badge.ok{background:#3fb950}
.badge.warn{background:#d29922}
.badge.fail{background:#f85149}
table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:20px}
th,td{padding:5px 8px;text-align:left;border-bottom:1px solid #21262d;white-space:nowrap}
th{background:#161b22;color:#8b949e;font-weight:500;position:sticky;top:0}
tr:hover{background:#1c2128}
td.desc{color:#8b949e;font-size:11px;white-space:normal;max-width:260px}
td.wrap{white-space:normal;font-size:11px;max-width:200px}
.tree-node{padding:2px 0;font-size:13px}
.tree-children{padding-left:22px;border-left:1px solid #30363d;margin-left:8px}
.tree-node.ok{color:#3fb950}
.tree-node.warn{color:#d29922}
.tree-node.fail{color:#f85149}
.pipeline-ok{color:#3fb950}
.pipeline-warn{color:#d29922}
.pipeline-error{color:#f85149}
.pipeline-ever{color:#8b949e}
.orphan{background:#2d1517!important}
.orphan td{color:#f85149}
.summary{display:flex;gap:20px;margin-bottom:16px;font-size:13px;color:#8b949e;flex-wrap:wrap}
.summary span strong{color:#c9d1d9}
.toggle{color:#58a6ff;cursor:pointer;font-size:11px;margin-left:8px}
.search-box{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;padding:6px 12px;border-radius:4px;margin-bottom:10px;width:300px;font-size:13px}
.cron-detail{font-size:11px;color:#8b949e;padding-left:20px}
.cron-tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;margin:0 2px}
.cron-tag.ok{background:#1a3a1a;color:#3fb950;border:1px solid #3fb950}
.cron-tag.warn{background:#3a2a1a;color:#d29922;border:1px solid #d29922}
.cron-tag.error{background:#3a1a1a;color:#f85149;border:1px solid #f85149}
.cron-tag.new{background:#1a1a3a;color:#58a6ff;border:1px solid #58a6ff}
</style></head>
<body>
<h1>📊 MoFin 系统健康监控</h1>
<div class="summary" id="summary">加载中...</div>
<div class="tabs">
<div class="tab active" onclick="switchTab(0)">🌳 功能树</div>
<div class="tab" onclick="switchTab(1)">🔧 全部流程/Cron (77)</div>
<div class="tab" onclick="switchTab(2)">🗃️ 数据实体</div>
</div>
<div id="panel0" class="panel active"></div>
<div id="panel1" class="panel"></div>
<div id="panel2" class="panel"></div>
<script>
let data = null;
function switchTab(idx) {
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',i==idx));
document.querySelectorAll('.panel').forEach((p,i)=>p.classList.toggle('active',i==idx));
}
function renderFeatureTree(tree) {
let html = '<table><thead><tr><th style="width:50%">功能模块</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody>';
function walk(node, depth) {
const indent = 'style="padding-left:'+(depth*20+8)+'px"';
const cls = node.status || 'ok';
// 尝试匹配cron
const pipeKey = (node.label||'').toLowerCase().split('(')[0].trim();
const pipes = node._pipes || [];
if (pipes.length === 0) {
// 无对应cron的功能节点
html += `<tr><td ${indent}><span class="badge ${cls}"></span><strong>${node.label}</strong><span class="cron-detail">(纯逻辑节点)</span></td>`;
html += `<td>-</td><td>-</td><td class="pipeline-${cls==='ok'?'ok':'warn'}">${cls}</td><td>-</td></tr>`;
} else {
// 有对应cron的节点
pipes.forEach((p, i) => {
const indent2 = i===0 ? indent : 'style="padding-left:'+(depth*20+32)+'px"';
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
if (i===0) {
html += `<tr><td ${indent2}><span class="badge ${cls}"></span><strong>${node.label}</strong></td>`;
} else {
html += `<tr><td ${indent2}><span style="color:#30363d">└</span> ${p.name||p.script||'LLM'}</td>`;
}
html += `<td><span class="cron-tag ${tagCls}">${p.type||'LLM'}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${p.status}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
}
if (node.children) node.children.forEach(c => walk(c, depth+1));
}
walk(tree, 0);
html += '</tbody></table>';
return html;
}
function renderPipelineTable(pipelines) {
let html = '<input class="search-box" id="pipeSearch" placeholder="搜索流程..." oninput="filterPipes()">';
html += '<table><thead><tr><th>名称</th><th>脚本/LLM</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody id="pipeBody">';
pipelines.forEach(p => {
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const statusDisplay = p.last_run ? p.status : '待首次运行';
html += `<tr class="pipe-row"><td>${p.name||p.script||'LLM'}</td>`;
html += `<td style="font-size:11px">${p.script||'LLM'}</td>`;
html += `<td><span class="cron-tag ${tagCls}">${p.type||cron}</span></td>`;
html += `<td style="font-size:11px">${p.schedule||'-'}</td>`;
html += `<td class="pipeline-${tagCls}">${statusDisplay}</td>`;
html += `<td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function filterPipes() {
const q = document.getElementById('pipeSearch').value.toLowerCase();
document.querySelectorAll('.pipe-row').forEach(r => {
r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
function loadData() {
fetch('/mofin_health.json?_='+Date.now())
.then(r=>r.json())
.then(d=>{
data = d;
// 把pipeline关联到feature tree节点
const pipeMap = {};
d.pipelines.forEach(p => {
const key = (p.name||p.script||'').toLowerCase().split('.')[0].trim();
const label = (p.name||p.script||'').toLowerCase();
pipeMap[key] = pipeMap[key] || [];
pipeMap[key].push(p);
// 也存label搜索
pipeMap[label] = pipeMap[label] || [];
pipeMap[label].push(p);
});
// 给feature tree节点挂_pipes
function attachPipes(node) {
const key = (node.label||'').toLowerCase().split('(')[0].trim();
node._pipes = pipeMap[key] || [];
// 尝试用脚本名匹配
if (!node._pipes.length) {
const scriptMatch = (node.label||'').match(/\(([^)]+)\)/);
if (scriptMatch) {
const s = scriptMatch[1].toLowerCase().split('.')[0];
node._pipes = pipeMap[s] || [];
}
}
if (node.children) node.children.forEach(c => attachPipes(c));
}
attachPipes(d.feature_tree);
// 统计
const cnt = {ok:0,warn:0,fail:0};
function countStatus(n) { cnt[n.status]++; if(n.children) n.children.forEach(countStatus); }
countStatus(d.feature_tree);
const pipeOk = d.pipelines.filter(p=>p.status==='ok').length;
const pipeErr = d.pipelines.filter(p=>p.status==='error'||p.status==='fail').length;
document.getElementById('summary').innerHTML =
`<span>生成: ${d.generated_at}</span>` +
`<span>功能: ✅${cnt.ok} ⚠️${cnt.warn}${cnt.fail}</span>` +
`<span>Cron: ✅${pipeOk}${pipeErr}${d.pipelines.length}</span>` +
`<span>数据表: ${d.entities.length} | 孤立: <strong style="color:#f85149">${d.entities.filter(e=>e.orphan).length}</strong></span>`;
// Tab 0: 功能树
document.getElementById('panel0').innerHTML = renderFeatureTree(d.feature_tree);
// Tab 1: 全部流程表
document.getElementById('panel1').innerHTML = renderPipelineTable(d.pipelines);
// Tab 2: 数据实体
let h2 = '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>状态</th></tr></thead><tbody>';
d.entities.forEach(e => {
const cls = e.orphan ? 'orphan' : '';
const badge = e.orphan ? '🔴 孤立' : (!e.has_input||!e.has_output) ? '⚠️ 单向' : '✅';
h2 += `<tr class="${cls}"><td><strong>${e.name}</strong></td><td class="desc">${e.desc||''}</td><td>${e.rows}</td>`;
h2 += `<td class="wrap">${(e.writers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td class="wrap">${(e.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td>${badge}</td></tr>`;
});
(d.json_files||[]).forEach(j => {
h2 += `<tr><td>📄 ${j.name}</td><td class="desc">${j.desc||'JSON文件'}</td><td>${j.size_kb}KB</td>`;
h2 += `<td class="wrap">JSON写入</td><td class="wrap">${(j.readers||[]).slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h2 += `<td>${j.warn?'⚠️':'✅'}</td></tr>`;
});
h2 += '</tbody></table>';
document.getElementById('panel2').innerHTML = h2;
})
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
}
loadData();
setInterval(loadData, 60000);
</script>
</body></html>