diff --git a/static/mofin_health.html b/static/mofin_health.html
index 26f17cf2..1eba3ea6 100644
--- a/static/mofin_health.html
+++ b/static/mofin_health.html
@@ -308,42 +308,62 @@ function renderDataFlow(entities, jsonFiles, architecture) {
}
function renderPipelineTable(pipelines) {
+ // 2026-08-12 三层架构分组呈现(采集→加工→使用→监控,老莫定)
+ const LAYERS = [
+ ['collect', '📥 数据采集层', '原始数据获取(K线/新闻/基本面/资金流/板块快照/宏观)'],
+ ['process', '⚙️ 数据加工层', '衍生指标计算(技术指标/行业指数/多周期均线/分位)'],
+ ['use', '📊 数据使用层', '策略扫描/评估/重评/候选管道(只读数据,不采集)'],
+ ['monitor', '🩺 监控运维层', '健康检查/审计/备份/推送/自修复'],
+ ];
+ const selStyle = 'background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:5px 8px;font-size:12px';
let html = '
';
html += '';
- html += '
';
- html += '| 名称 | 来源 | 脚本/LLM | 类型 | 调度 | 状态 | 最后运行 | 最后十次 |
';
- pipelines.forEach((p, idx) => {
- const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
- const statusDisplay = p.last_run ? p.status : '待首次运行';
- const profileBadge = p.profile==='position-analyst' ? '📋' : '📦';
- // cron 匹配 key:pipeline 中文名(服务端经 jobs.json 解析为 job id)+ 脚本名兜底
- const encName = encodeURIComponent(p.name || '');
- const encScript = encodeURIComponent(p.script || '');
- html += `| ${p.name||p.script||'LLM'} | `;
- html += `${profileBadge} ${p.profile||'?'} | `;
- html += `${p.script||'LLM'} | `;
- html += `${p.type||'cron'} | `;
- html += `${p.schedule||'-'} | `;
- html += `${statusDisplay} | `;
- html += `${p.last_run||'-'} | `;
- html += ` |
`;
+ html += `';
+ html += `';
+
+ LAYERS.forEach(([layer, title, desc]) => {
+ const group = pipelines.filter(p => (p.layer || 'use') === layer);
+ if (!group.length) return;
+ const ok = group.filter(p => p.status === 'ok').length;
+ const err = group.filter(p => p.status === 'error' || p.status === 'fail').length;
+ const layerColor = {collect:'#3fb950', process:'#d29922', use:'#58a6ff', monitor:'#bc8cff'}[layer];
+ html += ``;
+ html += `${title} ${desc} · ${group.length}任务 ✅${ok} ❌${err}
`;
+ html += '| 名称 | 来源 | 脚本/LLM | 类型 | 调度 | 状态 | 最后运行 | 最后十次 |
';
+ group.forEach((p) => {
+ const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
+ const statusDisplay = p.last_run ? p.status : '待首次运行';
+ const profileBadge = p.profile==='position-analyst' ? '📋' : '📦';
+ const encName = encodeURIComponent(p.name || '');
+ const encScript = encodeURIComponent(p.script || '');
+ html += `| ${p.name||p.script||'LLM'} | `;
+ html += `${profileBadge} ${p.profile||'?'} | `;
+ html += `${p.script||'LLM'} | `;
+ html += `${p.type||'cron'} | `;
+ html += `${p.schedule||'-'} | `;
+ html += `${statusDisplay} | `;
+ html += `${p.last_run||'-'} | `;
+ html += ` |
`;
+ });
+ html += '
';
});
- html += '
';
return html;
}
function filterPipes() {
const q = document.getElementById('pipeSearch').value.toLowerCase();
const profile = document.getElementById('pipeProfile').value;
+ const layer = (document.getElementById('pipeLayer')||{}).value || 'all';
document.querySelectorAll('.pipe-row').forEach(r => {
const name = r.getAttribute('data-name') || '';
const rp = r.getAttribute('data-profile') || 'default';
+ const rl = r.getAttribute('data-layer') || 'use';
const matchName = !q || name.includes(q);
const matchProfile = profile === 'all' || rp === profile;
- r.style.display = matchName && matchProfile ? '' : 'none';
+ const matchLayer = layer === 'all' || rl === layer;
+ r.style.display = matchName && matchProfile && matchLayer ? '' : 'none';
});
}
@@ -375,25 +395,44 @@ function loadData() {
// Tab 1: 全部流程表
document.getElementById('panel1').innerHTML = renderPipelineTable(d.pipelines);
- // Tab 2: 数据实体
- let h2 = '| 表名 | 作用 | 行数 | 写入方 | 读取方 | 数据流 |
';
- d.entities.forEach(e => {
- const flowMap = {healthy:'✅ 正常', write_only:'✏️ 写无读', read_only:'📖 读无写', orphan:'🔴 孤立'};
- const flowColors = {healthy:'#3fb950', write_only:'#d29922', read_only:'#58a6ff', orphan:'#f85149'};
- const badge = flowMap[e.flow_status] || '?';
- const color = flowColors[e.flow_status] || '#8b949e';
- const cls = e.orphan ? 'orphan' : '';
- h2 += `| ${e.name} | ${e.desc||''} | ${e.rows} | `;
- h2 += `${(e.writers||[]).slice(0,4).join(', ')||'无'} | `;
- h2 += `${(e.readers||[]).slice(0,4).join(', ')||'无'} | `;
- h2 += `${badge} |
`;
+ // Tab 2: 数据实体(2026-08-12 三层架构分组:采集层原始/加工层衍生/使用层业务)
+ const ENT_LAYERS = [
+ ['collect', '📥 采集层原始数据', '#3fb950'],
+ ['process', '⚙️ 加工层衍生数据', '#d29922'],
+ ['biz', '💼 使用层业务数据', '#58a6ff'],
+ ];
+ const flowMap = {healthy:'✅ 正常', write_only:'✏️ 写无读', read_only:'📖 读无写', orphan:'🔴 孤立'};
+ const flowColors = {healthy:'#3fb950', write_only:'#d29922', read_only:'#58a6ff', orphan:'#f85149'};
+ let h2 = '';
+ ENT_LAYERS.forEach(([layer, title, lcolor]) => {
+ const group = d.entities.filter(e => (e.layer || 'biz') === layer);
+ if (!group.length) return;
+ const warn = group.filter(e => e.flow_status !== 'healthy').length;
+ h2 += ``;
+ h2 += `${title} ${group.length}表 ⚠️${warn}非健康
`;
+ h2 += '| 表名 | 作用 | 行数 | 写入方 | 读取方 | 数据流 |
';
+ group.forEach(e => {
+ const badge = flowMap[e.flow_status] || '?';
+ const color = flowColors[e.flow_status] || '#8b949e';
+ const cls = e.orphan ? 'orphan' : '';
+ h2 += `| ${e.name} | ${e.desc||''} | ${e.rows} | `;
+ h2 += `${(e.writers||[]).slice(0,4).join(', ')||'无'} | `;
+ h2 += `${(e.readers||[]).slice(0,4).join(', ')||'无'} | `;
+ h2 += `${badge} |
`;
+ });
+ h2 += '
';
});
- (d.json_files||[]).forEach(j => {
- h2 += `| 📄 ${j.name} | ${j.desc||'JSON文件'} | ${j.size_kb}KB | `;
- h2 += `JSON写入 | ${(j.readers||[]).slice(0,4).join(', ')||'无'} | `;
- h2 += `${j.warn?'⚠️':'✅'} |
`;
- });
- h2 += '
';
+ // JSON 文件(归为兼容层)
+ if ((d.json_files||[]).length) {
+ h2 += `📄 JSON 兼容层 ${d.json_files.length}个(多数已迁移DB)
`;
+ h2 += '| 文件 | 作用 | 大小 | 写入 | 读取 | 状态 |
';
+ (d.json_files||[]).forEach(j => {
+ h2 += `| 📄 ${j.name} | ${j.desc||'JSON文件'} | ${j.size_kb}KB | `;
+ h2 += `JSON写入 | ${(j.readers||[]).slice(0,4).join(', ')||'无'} | `;
+ h2 += `${j.warn?'⚠️':'✅'} |
`;
+ });
+ h2 += '
';
+ }
document.getElementById('panel2').innerHTML = h2;
// Tab 3: 数据流