feat: 功能树+Cron合并视图+数据实体描述列

This commit is contained in:
知微
2026-07-08 12:23:34 +08:00
parent 398b6990b3
commit ee1231b32f
2 changed files with 182 additions and 0 deletions
+54
View File
@@ -19,6 +19,58 @@ CRON_FILES = [
"/home/hmo/.hermes/cron/jobs.json",
]
# 数据实体作用说明
TABLES_DESC = {
"holdings": "当前持仓(权威源)",
"holding_strategies": "每只股票的完整策略参数",
"portfolio_summary": "总资产/现金/仓位汇总",
"portfolio_state": "组合状态快照(只读派生)",
"strategy_evaluations": "策略重评历史记录",
"strategy_feedback": "策略效果反馈",
"watchlist_stocks": "自选股列表",
"candidates": "潜力股候选池(小果扫描产出)",
"live_prices": "所有持仓+自选最新实时价",
"price_events": "价格区间突破事件日志",
"market_snapshots": "大盘指数快照(每10分)",
"sector_snapshots": "行业板块数据",
"sector_signals": "行业信号(趋势检测产出)",
"signal_news": "信号相关新闻",
"macro_raw_news": "宏观新闻原始数据",
"macro_context_log": "宏观上下文(大盘偏向/指数)",
"stocks": "全量股票代码",
"stock_daily": "日线行情",
"stock_weekly": "周线行情",
"stock_monthly": "月线行情",
"stock_fundamentals": "基本面数据(PE/PB)",
"stock_sectors": "股票行业映射",
"capital_flow_cache": "资金流缓存",
"xiaoguo_scan_tracker": "小果扫描跟踪",
"advice_timeline": "建议执行时间线",
"accuracy_stats": "建议准确率统计",
"todos": "自愈任务队列",
"health_check_log": "健康检查日志",
"cash_log": "资金变动记录",
"mtf_cache": "多周期均线缓存",
"state_meta": "系统状态元数据",
}
JSON_DESC = {
"decisions.json": "策略决策(DB→JSON同步,兼容层)",
"portfolio.json": "持仓汇总(兼容层)",
"market.json": "市场概况数据",
"xiaoguo_insights.json": "小果分析洞察",
"candidate_pool.json": "潜力股候选池完整数据",
"zone_breach.json": "价格区间突破状态",
"strategy_staleness_report.json": "策略过期报告",
"alerts.json": "告警列表",
"macro_risk_state.json": "宏观风险状态(采集器写入)",
"capital_flow_cache.json": "资金流缓存",
"multi_tf_cache.json": "多周期均线缓存",
"macro_context.json": "宏观上下文JSON(旧兼容层)",
"system_inventory.json": "全量系统清单",
"mofin_health.json": "健康监控数据",
}
now = datetime.now()
def load_cron_jobs():
@@ -164,6 +216,7 @@ def build_report():
orphan = (not has_input or not has_output) and not is_system and cnt > 0
entities.append({
"name": tname,
"desc": TABLES_DESC.get(tname, ""),
"rows": cnt,
"readers": readers[:10],
"writers": writers[:10],
@@ -182,6 +235,7 @@ def build_report():
size = jf.stat().st_size / 1024
json_entities.append({
"name": jf.name,
"desc": JSON_DESC.get(jf.name, ""),
"size_kb": round(size, 1),
"readers": readers[:10],
"writers": [], # 难以精确追踪
+128
View File
@@ -0,0 +1,128 @@
<!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: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:5px}
.tree-label{display:flex;align-items:center;gap:6px;cursor:default}
.tree-label .name{color:#c9d1d9}
.orphan{background:#2d1517!important}
.orphan td{color:#f85149}
.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;flex-wrap:wrap}
.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>
<div id="panel0" class="panel active"></div>
<div id="panel1" 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 mergeView(featureTree, pipelines) {
const pipeMap = {};
pipelines.forEach(p => {
const key = (p.name || '').toLowerCase();
pipeMap[key] = p;
});
const html = ['<table><thead><tr><th>功能</th><th>Cron/流程</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 pipe = pipeMap[(node.label || '').toLowerCase().split('(')[0].trim()]
|| pipeMap[(node.label || '').toLowerCase().split(' ')[0]];
if (pipe) {
const cls = pipe.status=='ok' ? 'pipeline-ok' : pipe.status=='error' ? 'pipeline-error' : '';
const badge = pipe.status=='ok' ? '✅' : pipe.status=='error' ? '❌' : '❓';
html.push(`<tr><td ${indent}><span class="badge ${node.status}"></span>${node.label}</td>`);
html.push(`<td>${pipe.script||'LLM'}</td><td>${pipe.type}</td><td style="font-size:11px">${pipe.schedule||''}</td>`);
html.push(`<td class="${cls}">${badge} ${pipe.status}</td><td style="font-size:11px">${pipe.last_run||'-'}</td></tr>`);
} else {
html.push(`<tr><td ${indent} colspan="5"><span class="badge ${node.status}"></span>${node.label}</td></tr>`);
}
if (node.children) node.children.forEach(c => walk(c, depth+1));
}
walk(featureTree, 0);
html.push('</tbody></table>');
return html.join('');
}
function loadData() {
fetch('/mofin_health.json?_='+Date.now())
.then(r=>r.json())
.then(d=>{
data = d;
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);
document.getElementById('summary').innerHTML =
`<span>生成: ${d.generated_at}</span>` +
`<span>✅ ${cnt.ok} ⚠️ ${cnt.warn}${cnt.fail}</span>` +
`<span>数据表: ${d.entities.length} | 孤立: <strong style="color:#f85149">${d.entities.filter(e=>e.orphan).length}</strong></span>` +
`<span>Cron: ${d.pipelines.length}</span>`;
// Tab 0: Merged feature + pipeline
document.getElementById('panel0').innerHTML = mergeView(d.feature_tree, d.pipelines);
// Tab 1: Data entities with descriptions
let h1 = '<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) ? '⚠️ 单向' : '✅';
h1 += `<tr class="${cls}"><td><strong>${e.name}</strong></td><td class="desc">${e.desc||''}</td><td>${e.rows}</td>`;
h1 += `<td class="wrap">${e.writers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h1 += `<td class="wrap">${e.readers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h1 += `<td>${badge}</td></tr>`;
});
d.json_files.forEach(j => {
h1 += `<tr class="${j.warn?'warn-row':''}"><td>📄 ${j.name}</td><td class="desc">${j.desc||'JSON文件'}</td><td>${j.size_kb}KB</td>`;
h1 += `<td class="wrap">JSON写入</td><td class="wrap">${j.readers.slice(0,4).join(', ')||'<span style="color:#f85149">无</span>'}</td>`;
h1 += `<td>${j.warn?'⚠️':'✅'}</td></tr>`;
});
h1 += '</tbody></table>';
document.getElementById('panel1').innerHTML = h1;
})
.catch(e => document.getElementById('summary').innerHTML = '❌ 加载失败: ' + e.message);
}
loadData();
setInterval(loadData, 60000);
</script>
</body></html>