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 += ''; - 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 += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - html += ``; - 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类型调度状态最后运行最后十次
${p.name||p.script||'LLM'}${profileBadge} ${p.profile||'?'}${p.script||'LLM'}${p.type||'cron'}${p.schedule||'-'}${statusDisplay}${p.last_run||'-'}
'; + 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 += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + html += ``; + }); + html += '
名称来源脚本/LLM类型调度状态最后运行最后十次
${p.name||p.script||'LLM'}${profileBadge} ${p.profile||'?'}${p.script||'LLM'}${p.type||'cron'}${p.schedule||'-'}${statusDisplay}${p.last_run||'-'}
'; }); - 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 += ``; - h2 += ``; - h2 += ``; - h2 += ``; + // 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 += '
表名作用行数写入方读取方数据流
${e.name}${e.desc||''}${e.rows}${(e.writers||[]).slice(0,4).join(', ')||''}${(e.readers||[]).slice(0,4).join(', ')||''}${badge}
'; + group.forEach(e => { + const badge = flowMap[e.flow_status] || '?'; + const color = flowColors[e.flow_status] || '#8b949e'; + const cls = e.orphan ? 'orphan' : ''; + h2 += ``; + h2 += ``; + h2 += ``; + h2 += ``; + }); + h2 += '
表名作用行数写入方读取方数据流
${e.name}${e.desc||''}${e.rows}${(e.writers||[]).slice(0,4).join(', ')||''}${(e.readers||[]).slice(0,4).join(', ')||''}${badge}
'; }); - (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 += ``; + h2 += ``; + h2 += ``; + }); + h2 += '
文件作用大小写入读取状态
📄 ${j.name}${j.desc||'JSON文件'}${j.size_kb}KBJSON写入${(j.readers||[]).slice(0,4).join(', ')||''}${j.warn?'⚠️':'✅'}
'; + } document.getElementById('panel2').innerHTML = h2; // Tab 3: 数据流