feat: F健康三层架构改造step2——前端Tab1(Cron)/Tab2(数据实体)按采集/加工/使用三层分组呈现+层筛选,忠实反映新架构

This commit is contained in:
hmo
2026-08-12 10:15:44 +08:00
parent 9577e49762
commit c3687aa4da
+79 -40
View File
@@ -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 = '<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center">';
html += '<input class="search-box" id="pipeSearch" placeholder="搜索流程..." oninput="filterPipes()" style="flex:1">';
html += '<select id="pipeProfile" onchange="filterPipes()" style="background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:4px;padding:5px 8px;font-size:12px">';
html += '<option value="position-analyst">📋 知微</option>';
html += '<option value="all">📋+📦 全部</option>';
html += '</select></div>';
html += '<table><thead><tr><th>名称</th><th>来源</th><th>脚本/LLM</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th><th>最后十次</th></tr></thead><tbody id="pipeBody">';
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 匹配 keypipeline 中文名(服务端经 jobs.json 解析为 job id+ 脚本名兜底
const encName = encodeURIComponent(p.name || '');
const encScript = encodeURIComponent(p.script || '');
html += `<tr class="pipe-row" data-profile="${p.profile||'default'}" data-name="${(p.name||'').toLowerCase()}"><td>${p.name||p.script||'LLM'}</td>`;
html += `<td style="font-size:11px">${profileBadge} ${p.profile||'?'}</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>`;
html += `<td><button class="pipe-reports-btn" data-cron="${encName}" data-script="${encScript}" data-name="${(p.name||'').replace(/"/g,'&quot;')}" onclick="openLastReports(this)">最后十次</button></td></tr>`;
html += `<select id="pipeLayer" onchange="filterPipes()" style="${selStyle}">`;
html += '<option value="all">全部层</option><option value="collect">📥 采集层</option><option value="process">⚙️ 加工层</option><option value="use">📊 使用层</option><option value="monitor">🩺 监控层</option></select>';
html += `<select id="pipeProfile" onchange="filterPipes()" style="${selStyle}">`;
html += '<option value="position-analyst">📋 知微</option><option value="all">📋+📦 全部</option></select></div>';
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 += `<div style="margin:14px 0 6px;padding:8px 12px;background:#161b22;border-left:3px solid ${layerColor};border-radius:4px">`;
html += `<strong style="color:${layerColor}">${title}</strong> <span style="color:#8b949e;font-size:11px">${desc} · ${group.length}任务 ✅${ok}${err}</span></div>`;
html += '<table><thead><tr><th>名称</th><th>来源</th><th>脚本/LLM</th><th>类型</th><th>调度</th><th>状态</th><th>最后运行</th><th>最后十次</th></tr></thead><tbody>';
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 += `<tr class="pipe-row" data-profile="${p.profile||'default'}" data-layer="${p.layer||'use'}" data-name="${(p.name||'').toLowerCase()}"><td>${p.name||p.script||'LLM'}</td>`;
html += `<td style="font-size:11px">${profileBadge} ${p.profile||'?'}</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>`;
html += `<td><button class="pipe-reports-btn" data-cron="${encName}" data-script="${encScript}" data-name="${(p.name||'').replace(/"/g,'&quot;')}" onclick="openLastReports(this)">最后十次</button></td></tr>`;
});
html += '</tbody></table>';
});
html += '</tbody></table>';
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 = '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>数据流</th></tr></thead><tbody>';
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 += `<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 style="color:${color}">${badge}</td></tr>`;
// 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 += `<div style="margin:14px 0 6px;padding:8px 12px;background:#161b22;border-left:3px solid ${lcolor};border-radius:4px">`;
h2 += `<strong style="color:${lcolor}">${title}</strong> <span style="color:#8b949e;font-size:11px">${group.length}表 ⚠️${warn}非健康</span></div>`;
h2 += '<table><thead><tr><th>表名</th><th>作用</th><th>行数</th><th>写入方</th><th>读取方</th><th>数据流</th></tr></thead><tbody>';
group.forEach(e => {
const badge = flowMap[e.flow_status] || '?';
const color = flowColors[e.flow_status] || '#8b949e';
const cls = e.orphan ? 'orphan' : '';
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 style="color:${color}">${badge}</td></tr>`;
});
h2 += '</tbody></table>';
});
(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>';
// JSON 文件(归为兼容层)
if ((d.json_files||[]).length) {
h2 += `<div style="margin:14px 0 6px;padding:8px 12px;background:#161b22;border-left:3px solid #8b949e;border-radius:4px"><strong style="color:#8b949e">📄 JSON 兼容层</strong> <span style="color:#8b949e;font-size:11px">${d.json_files.length}个(多数已迁移DB</span></div>`;
h2 += '<table><thead><tr><th>文件</th><th>作用</th><th>大小</th><th>写入</th><th>读取</th><th>状态</th></tr></thead><tbody>';
(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;
// Tab 3: 数据流