feat: introduce spec system + dashboard + health pipeline (AgentsMeeting template)

This commit is contained in:
hmo
2026-07-19 10:50:34 +08:00
parent 81c6dd2314
commit 747fdfe467
19 changed files with 1826 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>MoFin Dashboard</title>
<style>
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#c9d1d9;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d29922}
*{margin:0;padding:0;box-sizing:border-box}body{background:var(--bg);color:var(--text);font:20px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:28px}
h1{font-size:32px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitle{color:var(--dim);font-size:18px;margin-bottom:20px}
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.tab-btn{padding:12px 24px;cursor:pointer;font-size:19px;color:var(--dim);border:none;background:none;border-bottom:2px solid transparent}
.tab-btn:hover{color:var(--text)}.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab-pane{display:none}.tab-pane.active{display:block}
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}
.stat-c{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px}
.stat-c .n{font-size:42px;font-weight:700}.stat-c .l{font-size:18px;color:var(--dim)}
.stat-c.g .n{color:var(--green)}.stat-c.r .n{color:var(--red)}.stat-c.y .n{color:var(--yellow)}
.section{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px}
.section h2{font-size:19px;font-weight:600;color:var(--accent);display:flex;align-items:center}
.help-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;border-radius:50%;border:1px solid var(--border);background:var(--card);color:var(--dim);cursor:pointer;font-size:14px;line-height:1;margin-left:8px;flex-shrink:0}
.help-btn:hover{border-color:var(--accent);color:var(--accent)}
.help-btn.ai{color:var(--yellow);border-color:var(--yellow);font-size:12px}
.help-btn.ai:hover{background:var(--yellow);color:#000}
.spec-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center}
.spec-modal.show{display:flex}
.spec-modal .modal-box{background:var(--card);border:1px solid var(--border);border-radius:12px;max-width:700px;max-height:80vh;overflow-y:auto;padding:28px;margin:20px}
.spec-modal .modal-box h3{font-size:22px;color:var(--accent);margin-bottom:12px}
.spec-modal .modal-box .close{float:right;cursor:pointer;color:var(--dim);font-size:22px}
.spec-modal .modal-box .close:hover{color:var(--text)}
.spec-modal .modal-box h4{font-size:19px;color:var(--accent);margin:16px 0 6px}
.spec-modal .modal-box p{font-size:18px;color:var(--text);margin:4px 0}
.spec-modal .modal-box ul{margin:4px 0 4px 20px}
.spec-modal .modal-box li{font-size:18px;color:var(--text);margin:2px 0}
.spec-modal .modal-box pre{background:var(--bg);padding:12px;border-radius:6px;font-size:15px;overflow-x:auto;margin:8px 0}
.svc-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border)}
.svc-row:last-child{border-bottom:none}
.svc-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.svc-dot.ok{background:var(--green)}.svc-dot.fail{background:var(--red)}
.svc-name{flex:1;min-width:0;font-size:18px}
.svc-status{font-size:16px;font-weight:600}
.svc-status.ok{color:var(--green)}.svc-status.fail{color:var(--red)}
.svc-meta{font-size:16px;color:var(--dim)}
.sub-bar{display:flex;gap:2px;margin-bottom:10px;flex-wrap:wrap}
.sub-btn{padding:4px 12px;font-size:18px;background:0 0;border:1px solid transparent;border-radius:12px;color:var(--dim);cursor:pointer}
.sub-btn:hover{color:var(--text);border-color:var(--border)}.sub-btn.active{color:var(--accent);border-color:var(--accent);background:var(--bg)}
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:19px;z-index:999;opacity:0;pointer-events:none;transition:opacity .3s}
.toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff}
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:16px;font-weight:600;margin-left:6px}
.tag.g{background:#3fb95020;color:var(--green)}.tag.r{background:#f8514920;color:var(--red)}
.tag.y{background:#d2992220;color:var(--yellow)}
.task-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
.task-row:last-child{border-bottom:none}
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:16px}
.section p,.section li{font-size:18px;line-height:1.8}.section code{font-size:18px}
</style></head><body>
<h1>MoFin</h1><div class="subtitle">持仓情报系统 · Dashboard</div>
<div class="tabs" id="tabs"></div><div id="panes"></div>
<div id="toast" class="toast"></div>
<div id="spec-modal" class="spec-modal"><div class="modal-box" id="spec-modal-box"></div></div>
<script>
var T=[
{id:"services",label:"Services"},
{id:"principles",label:"开发原则",children:[
{id:"spec",label:"G 规范"},
{id:"health",label:"F 健康"}
]}
];
var at=localStorage.getItem('mt')||'services',as=localStorage.getItem('ms')||'spec';
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'<').replace(/>/g,'>')}
function qs(id){return document.getElementById(id)}
function toast(m,t){var e=qs('toast');e.textContent=m;e.className='toast show '+(t||'ok');setTimeout(function(){e.className='toast'},2500)}
// ---- Spec System ----
var _specCache={};
function showModuleHelp(module,type){
var modal=qs('spec-modal');var box=qs('spec-modal-box');
if(_specCache[module]){_renderSpecModal(_specCache[module],type);modal.classList.add('show');return}
fetch('/api/module-spec/'+module).then(function(r){return r.json()}).then(function(spec){
_specCache[module]=spec;_renderSpecModal(spec,type);modal.classList.add('show');
}).catch(function(){
box.innerHTML='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">&times;</span><h3>Spec not found</h3><p>Module: '+esc(module)+'</p>';modal.classList.add('show');
});
}
function _renderSpecModal(spec,type){
var box=qs('spec-modal-box');var h='';
h+='<span class="close" onclick="qs(\'spec-modal\').classList.remove(\'show\')">&times;</span>';
if(type==='human'){
var hh=spec.human_help||{};
h+='<h3>'+esc(hh.title||spec.module||'Help')+'</h3>';
if(hh.description){h+='<h4>说明</h4><ul>';hh.description.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
if(hh.usage){h+='<h4>使用方法</h4><ul>';hh.usage.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
if(hh.troubleshooting){h+='<h4>常见问题</h4><ul>';hh.troubleshooting.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
}else{
var ai=spec.ai_spec||{};
h+='<h3>§ AI Spec — '+esc(spec.module||'')+'</h3>';
h+='<p style="color:var(--dim)">'+esc(spec.purpose||'')+'</p>';
if(ai.apis){h+='<h4>API</h4><ul>';ai.apis.forEach(function(a){h+='<li><code>'+esc(a.method)+' '+esc(a.path)+'</code> — '+esc(a.returns||'')+'</li>'});h+='</ul>'}
if(ai.dependencies){h+='<h4>依赖</h4><ul>';ai.dependencies.forEach(function(d){h+='<li>'+esc(d)+'</li>'});h+='</ul>'}
if(ai.constraints){h+='<h4>关键约束</h4><ul>';ai.constraints.forEach(function(d){h+='<li style="color:var(--yellow)">'+esc(d)+'</li>'});h+='</ul>'}
if(ai.must_not){h+='<h4>禁止行为</h4><ul>';ai.must_not.forEach(function(d){h+='<li style="color:var(--red)">'+esc(d)+'</li>'});h+='</ul>'}
if(ai.related_files){h+='<h4>相关文件</h4><ul>';ai.related_files.forEach(function(f){h+='<li><code>'+esc(f)+'</code></li>'});h+='</ul>'}
}
box.innerHTML=h;
}
// ---- Tab System ----
function init(){
var b='',p='';
for(var i=0;i<T.length;i++){
var t=T[i],a=t.id===at||(t.children&&t.children.some(function(c){return c.id===at}));
b+='<button class="tab-btn'+(a?' active':'')+'" onclick="sw(\''+t.id+'\')">'+t.label+'</button>';
if(t.children){
p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div class="sub-bar">';
for(var j=0;j<t.children.length;j++){
var c=t.children[j];
p+='<button class="sub-btn'+(c.id===as?' active':'')+'" onclick="sws(\''+c.id+'\')">'+c.label+'</button>';
}
p+='</div>';
for(var j=0;j<t.children.length;j++){
var c=t.children[j];
p+='<div id="sp-'+c.id+'"'+(c.id===as?'':' style="display:none"')+'><div id="ct-'+c.id+'"><span style="color:var(--dim)">Loading...</span></div></div>';
}
p+='</div>';
}else{
p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span style="color:var(--dim)">Loading...</span></div></div>';
}
}
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;
}
function sw(id){
if(id==='principles'){at='principles';as=localStorage.getItem('ms')||'spec'}
else{at=id;localStorage.setItem('mt',id)}
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))}).label)});
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pn-'+id)});
if(id==='principles'){document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[1].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+as?'':'none'});(FETCHES[as]||function(){})()}
}
function sws(id){as=id;localStorage.setItem('ms',id);document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[1].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+id?'':'none'});(FETCHES[id]||function(){})()}
function fill(id,h){var e=qs('ct-'+id);if(e)e.innerHTML=h}
// ====== Services Tab ======
async function loadServices(){
try{
var r=await fetch('/api/services'),d=await r.json();
var svcs=d.services||[],ok=d.summary.ok||0,total=d.summary.total||0;
var hasIssue=ok<total,allOk=ok===total;
var h='<h2>服务状态<span class="help-btn" onclick="showModuleHelp(\'dashboard\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'dashboard\',\'ai\')" title="AI Spec">§</span></h2>';
h+='<div class="stats"><div class="stat-c '+(allOk?'g':'r')+'"><div class="n">'+ok+'/'+total+'</div><div class="l">Services Online</div></div></div>';
h+='<div class="section"><h2>全部服务</h2>';
// Group by layer
var layers={};
svcs.forEach(function(s){
var l=s.layer||'Other';
if(!layers[l])layers[l]=[];
layers[l].push(s);
});
var layerOrder=['通信层','核心服务','AI 网关','数据层','Other'];
layerOrder.forEach(function(ln){
if(!layers[ln])return;
h+='<div style="margin-bottom:10px"><div style="color:var(--dim);font-size:16px;margin-bottom:4px">'+esc(ln)+'</div>';
layers[ln].forEach(function(s){
var hOk=s.health&&s.health.ok;
h+='<div class="svc-row">';
h+='<div class="svc-dot '+(hOk?'ok':'fail')+'"></div>';
h+='<div class="svc-name">'+esc(s.label||s.name);
if(s.port)h+=' <span class="mono">:'+s.port+'</span>';
h+='</div>';
h+='<div class="svc-meta">'+esc(s.type)+'</div>';
h+='<div class="svc-status '+(hOk?'ok':'fail')+'">'+(hOk?'UP':'DOWN')+'</div>';
h+='<span class="help-btn" onclick="showModuleHelp(\''+esc(s.name)+'\',\'human\')" title="帮助">?</span>';
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+esc(s.name)+'\',\'ai\')" title="AI Spec">§</span>';
h+='</div>';
});
h+='</div>';
});
h+='</div>';
fill('services',h);
}catch(e){fill('services','<div class="section"><span style="color:var(--red)">Failed to load services: '+esc(e.message)+'</span></div>')}
}
// ====== G 规范 Tab ======
async function loadSpec(){
var h='<div class="section">';
h+='<h2>开发规范<span class="help-btn" onclick="showModuleHelp(\'dashboard\',\'human\')" title="说明">?</span></h2>';
h+='<p>MoFin 开发规范定义了五条红线和 spec 双轨同源体系。</p>';
h+='<h4 style="color:var(--accent);margin-top:12px">五条红线</h4>';
h+='<ol><li>先读/写 Spec,再写代码</li><li>部署必验</li><li>不可见即不存在</li><li>实现后同步 Spec</li><li>部署目标即验收标准(Linux 246</li></ol>';
h+='<h4 style="color:var(--accent);margin-top:12px">模块 Spec 清单</h4>';
h+='<p>以下模块已有 spec 文件,点击 ?§ 查看详情:</p>';
var modules=[
{id:'portfolio',label:'持仓管理'},
{id:'watchlist',label:'自选股'},
{id:'decisions',label:'策略决策库'},
{id:'market',label:'市场观察'},
{id:'signals',label:'信号与扫描'},
{id:'evaluation',label:'策略评估'},
{id:'dashboard',label:'Dashboard'},
{id:'health',label:'健康监控'}
];
h+='<div style="margin-top:8px">';
modules.forEach(function(m){
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 0">';
h+='<code style="flex:1">'+esc(m.id)+'</code><span style="color:var(--dim)">'+esc(m.label)+'</span>';
h+='<span class="help-btn" onclick="showModuleHelp(\''+esc(m.id)+'\',\'human\')" title="人类帮助">?</span>';
h+='<span class="help-btn ai" onclick="showModuleHelp(\''+esc(m.id)+'\',\'ai\')" title="AI Spec">§</span>';
h+='</div>';
});
h+='</div>';
h+='<p style="margin-top:12px;font-size:16px;color:var(--dim)">完整规范见 <code>docs/dev-spec.md</code> 和 <code>docs/DEVELOPMENT_STANDARDS.md</code></p>';
h+='</div>';
fill('spec',h);
}
// ====== F 健康 Tab ======
async function loadHealth(){
try{
var r=await fetch('/api/monitor'),d=await r.json();
var tasks=d.tasks||[],tier1=d.tier1||{},tier2=d.tier2||{};
var h='<h2>系统健康<span class="help-btn" onclick="showModuleHelp(\'health\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'health\',\'ai\')" title="AI Spec">§</span></h2>';
// Summary
var t1Ok=tier1.summary?tier1.summary.ok||0:0;
var t1Total=tier1.summary?tier1.summary.total||0:0;
var allOk=t1Ok===t1Total&&t1Total>0;
h+='<div class="stats"><div class="stat-c '+(allOk?'g':'y')+'"><div class="n">'+t1Ok+'/'+t1Total+'</div><div class="l">Tier1 通过</div></div></div>';
// Tasks
h+='<div class="section"><h2>定时任务</h2>';
tasks.forEach(function(t){
var cls=t.status==='cron_ok'||t.status==='running'?'g':(t.status==='not_deployed'?'y':'r');
var label=t.status==='cron_ok'?'正常':(t.status==='running'?'运行中':(t.status==='not_deployed'?'未部署':t.status));
h+='<div class="task-row">';
h+='<div class="tag '+cls+'">'+esc(label)+'</div>';
h+='<div>'+esc(t.name)+'</div>';
if(t.detail)h+='<div style="color:var(--dim);font-size:16px">'+esc(t.detail)+'</div>';
h+='</div>';
});
h+='</div>';
// Tier1 details
if(tier1.services&&tier1.services.length){
h+='<div class="section"><h2>Tier1 服务详情</h2>';
tier1.services.forEach(function(s){
var ok=s.health&&s.health.ok;
h+='<div class="svc-row"><div class="svc-dot '+(ok?'ok':'fail')+'"></div><div class="svc-name">'+esc(s.label||s.name)+'</div><div class="svc-status '+(ok?'ok':'fail')+'">'+(ok?'OK':'FAIL')+'</div></div>';
});
h+='</div>';
}
h+='<p style="font-size:16px;color:var(--dim);margin-top:8px">生成时间: '+esc(d.generated_at||'')+'</p>';
fill('health',h);
}catch(e){fill('health','<div class="section"><span style="color:var(--red)">Failed to load health data: '+esc(e.message)+'</span></div>')}
}
// ---- Fetch dispatch ----
var FETCHES={'services':loadServices,'spec':loadSpec,'health':loadHealth};
// ---- Auto-refresh ----
function autoRefresh(){
var activeTab=localStorage.getItem('mt')||'services';
if(activeTab==='services')loadServices();
else if(activeTab==='principles'){
var sub=localStorage.getItem('ms')||'spec';
if(sub==='health')loadHealth();
}
}
// ---- Boot ----
init();
// Load initial tab
var tab=localStorage.getItem('mt')||'services';
if(tab==='services')loadServices();
else if(tab==='principles'){
var sub=localStorage.getItem('ms')||'spec';
if(sub==='spec')loadSpec();
else if(sub==='health')loadHealth();
}
// Auto-refresh every 5 seconds
setInterval(autoRefresh,5000);
</script></body></html>