feat: introduce spec system + dashboard + health pipeline (AgentsMeeting template)
This commit is contained in:
@@ -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,'&').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\')">×</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\')">×</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>
|
||||
Reference in New Issue
Block a user