Files
MoFin/templates/dashboard.html
T

278 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>