- 量价分析: full_analysis输出volume_deep+成交量存储在price_history.json - FK约束移除: holding_strategies外键->holdings阻止自选股写入 - #000850 重评已写入(止损3.74/止盈4.06/RR1.67)含量价信号
131 lines
6.2 KiB
HTML
131 lines
6.2 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}
|
|
.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:6px 10px;text-align:left;border-bottom:1px solid #21262d}
|
|
th{background:#161b22;color:#8b949e;font-weight:500;position:sticky;top:0}
|
|
tr:hover{background:#1c2128}
|
|
.tree-node{padding:3px 0;font-size:13px}
|
|
.tree-children{padding-left:24px;border-left:1px solid #30363d;margin-left:5px}
|
|
.tree-label{display:flex;align-items:center;gap:6px;cursor:default}
|
|
.tree-label .name{color:#c9d1d9}
|
|
.tree-label .count{color:#8b949e;font-size:11px}
|
|
.orphan{background:#2d1517!important}
|
|
.orphan td{color:#f85149}
|
|
.warn-row td{color:#d29922}
|
|
.pipeline-ok{color:#3fb950}
|
|
.pipeline-error{color:#f85149}
|
|
.pipeline-unknown{color:#8b949e}
|
|
.summary{display:flex;gap:20px;margin-bottom:16px;font-size:13px;color:#8b949e}
|
|
.summary span strong{color:#c9d1d9}
|
|
</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)">🗃️ 数据实体</div>
|
|
<div class="tab" onclick="switchTab(2)">🔧 流程/Cron</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 renderTree(node) {
|
|
const d = document.createElement('div');
|
|
d.className = 'tree-node';
|
|
const label = document.createElement('div');
|
|
label.className = 'tree-label';
|
|
label.innerHTML = `<span class="badge ${node.status}"></span><span class="name">${node.label}</span>`;
|
|
d.appendChild(label);
|
|
if (node.children) {
|
|
const ch = document.createElement('div');
|
|
ch.className = 'tree-children';
|
|
node.children.forEach(c => ch.appendChild(renderTree(c)));
|
|
d.appendChild(ch);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function loadData() {
|
|
fetch('/data/mofin_health.json?_='+Date.now())
|
|
.then(r=>r.json())
|
|
.then(d=>{
|
|
data = d;
|
|
// Summary
|
|
const counts = {ok:0,warn:0,fail:0};
|
|
function countStatus(n) { counts[n.status]++; if(n.children) n.children.forEach(countStatus); }
|
|
countStatus(d.feature_tree);
|
|
document.getElementById('summary').innerHTML =
|
|
`<span>生成: ${d.generated_at}</span>` +
|
|
`<span>功能树: <strong style="color:#3fb950">${counts.ok}✅</strong> <strong style="color:#d29922">${counts.warn}⚠️</strong> <strong style="color:#f85149">${counts.fail}❌</strong></span>` +
|
|
`<span>数据表: <strong>${d.entities.length}</strong> | 孤立: <strong style="color:#f85149">${d.entities.filter(e=>e.orphan).length}</strong></span>` +
|
|
`<span>Cron: <strong>${d.pipelines.length}</strong></span>`;
|
|
|
|
// Tab 0: Feature tree
|
|
document.getElementById('panel0').appendChild(renderTree(d.feature_tree));
|
|
|
|
// Tab 1: Entities
|
|
let html1 = '<table><thead><tr><th>表名</th><th>行数</th><th>输入(写入方)</th><th>输出(读取方)</th><th>状态</th></tr></thead><tbody>';
|
|
d.entities.forEach(e => {
|
|
const cls = e.orphan ? 'orphan' : (!e.has_input||!e.has_output) ? 'warn-row' : '';
|
|
const badge = e.orphan ? '🔴 孤立' : (!e.has_input||!e.has_output) ? '⚠️ 单向' : '✅';
|
|
html1 += `<tr class="${cls}">`;
|
|
html1 += `<td><strong>${e.name}</strong></td><td>${e.rows}</td>`;
|
|
html1 += `<td style="font-size:11px">${e.writers.slice(0,5).join(', ') || '<span style="color:#f85149">无写入</span>'}</td>`;
|
|
html1 += `<td style="font-size:11px">${e.readers.slice(0,5).join(', ') || '<span style="color:#f85149">无读取</span>'}</td>`;
|
|
html1 += `<td>${badge}</td></tr>`;
|
|
});
|
|
// JSON files section
|
|
d.json_files.forEach(j => {
|
|
const cls = j.warn ? 'warn-row' : '';
|
|
html1 += `<tr class="${cls}"><td>📄 ${j.name}</td><td>${j.size_kb}KB</td>`;
|
|
html1 += `<td style="font-size:11px">JSON文件</td>`;
|
|
html1 += `<td style="font-size:11px">${j.readers.slice(0,5).join(', ') || '<span style="color:#f85149">无读取</span>'}</td>`;
|
|
html1 += `<td>${j.warn ? '⚠️ 无人读' : '✅'}</td></tr>`;
|
|
});
|
|
html1 += '</tbody></table>';
|
|
document.getElementById('panel1').innerHTML = html1;
|
|
|
|
// Tab 2: Pipelines
|
|
let html2 = '<table><thead><tr><th>流程名</th><th>类型</th><th>脚本</th><th>调度</th><th>状态</th><th>最后运行</th></tr></thead><tbody>';
|
|
d.pipelines.forEach(p => {
|
|
const cls = p.status=='ok' ? 'pipeline-ok' : p.status=='error' ? 'pipeline-error' : 'pipeline-unknown';
|
|
const badge = p.status=='ok' ? '✅' : p.status=='error' ? '❌' : '❓';
|
|
html2 += `<tr><td>${p.name}</td><td>${p.type}</td><td style="font-size:11px">${p.script||'LLM'}</td><td style="font-size:11px">${p.schedule}</td>`;
|
|
html2 += `<td class="${cls}">${badge} ${p.status}</td><td style="font-size:11px">${p.last_run||'-'}</td></tr>`;
|
|
});
|
|
html2 += '</tbody></table>';
|
|
document.getElementById('panel2').innerHTML = html2;
|
|
})
|
|
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
|
|
}
|
|
|
|
loadData();
|
|
setInterval(loadData, 60000); // 每分钟刷新
|
|
</script>
|
|
</body></html>
|