<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>AgentsMeeting 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)}
.agents{display:flex;flex-direction:column;gap:12px}
.ac{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden}
.ac.online{border-left:3px solid var(--green)}.ac.degraded{border-left:3px solid var(--yellow)}
.ac.offline{border-left:3px solid var(--red);opacity:.7}
.ah{display:flex;align-items:center;padding:14px 16px;gap:12px;cursor:pointer}
.ah:hover{background:rgba(255,255,255,.02)}
.sd{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.sd.g{background:var(--green);box-shadow:0 0 6px var(--green)}
.sd.y{background:var(--yellow)}.sd.r{background:var(--red)}
.ai{flex:1;min-width:0}.an{font-size:19px;font-weight:600}
.am{font-size:18px;color:var(--dim);margin-top:2px}
.b{display:inline-block;padding:2px 10px;border-radius:10px;font-size:18px;font-weight:600;margin-left:8px}
.b.w{background:#0078d420;color:#60cdff}.b.l{background:#fcc62420;color:#fcc624}.b.m{background:#99999920;color:#ccc}
.as{display:flex;gap:16px;align-items:center;padding-right:8px}
.as .v{font-size:19px;font-weight:600}.as .l{font-size:18px;color:var(--dim)}
.aa{display:flex;gap:4px}
.btn{padding:6px 14px;border:1px solid var(--border);border-radius:5px;background:#21262d;color:var(--text);cursor:pointer;font-size:19px}
.btn:hover{background:#30363d}
.btn.s{border-color:var(--green);color:var(--green)}
.btn.x{border-color:var(--red);color:var(--red)}
.btn.r{border-color:var(--yellow);color:var(--yellow)}
.btn.l{border-color:var(--accent);color:var(--accent)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.svcs{display:flex;gap:8px;padding:0 16px 10px;flex-wrap:wrap}
.st{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:12px;font-size:19px;background:#0d1117;border:1px solid var(--border)}
.st .d{width:6px;height:6px;border-radius:50%}.st .d.running{background:var(--green)}.st .d.stopped{background:var(--dim)}
.st .p{color:var(--accent);font-family:monospace;font-size:18px;margin-left:4px}
.i{display:flex;align-items:center;gap:6px;margin:4px 0}
.sn{flex:1;min-width:0}
.lp{display:none;border-top:1px solid var(--border);padding:12px 16px;background:#0a0e14}
.lp.open{display:block}.lh{font-size:18px;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between}
.lc{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);max-height:300px;overflow-y:auto;background:#06080c;border:1px solid var(--border);border-radius:6px;padding:10px;white-space:pre-wrap}
.help-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;max-width:20px;min-height:20px;max-height: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;overflow:hidden}
.i .help-btn{margin-left: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}
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:19px;z-index:999;opacity:0;pointer-events:none}
.toast.show{opacity:1}.toast.ok{background:#238636;color:#fff}.toast.err{background:#da3633;color:#fff}
.ps{margin-top:24px}.ps h2{font-size:18px;color:var(--dim);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center}
.psv{display:flex;gap:8px;flex-wrap:wrap}
.psv .i{background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;display:flex;align-items:center;gap:8px;font-size:18px}
h2{display:flex;align-items:center}
.psv .i .d{width:6px;height:6px;border-radius:50%}.psv .i .d.ok{background:var(--green)}.psv .i .d.stopped{background:var(--dim)}
.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}
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:18px}
.ar{font-size:16px;color:var(--yellow);margin-top:4px}
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:19px;font-weight:600}
.tag.g{background:#3fb95020;color:var(--green)}.tag.r{background:#f8514920;color:var(--red)}
.tag.y{background:#d2992220;color:var(--yellow)}.tag.d{background:#8b949e20;color:var(--dim)}
table{width:100%;border-collapse:collapse;font-size:18px}th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
th{color:var(--dim);font-weight:600;font-size:19px}
.log-line{font:16px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);padding:2px 0}
.sr{display:flex;align-items:center;gap:10px;padding:5px 0}
.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)}
@media(max-width:768px){.g2{grid-template-columns:1fr}}
.section{font-size:18px;line-height:1.8}.section p,.section li{font-size:18px;line-height:1.8}.section code{font-size:18px}.section pre{font-size:18px;line-height:1.5}
</style></head><body>
<h1>AgentsMeeting</h1><div class="subtitle" id="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":"agents","label":"Agents"},{"id":"kanban","label":"Kanban"},{"id":"infra","label":"Infrastructure"},{"id":"principles","label":"开发原则","children":[{"id":"spec","label":"G 规范"},{"id":"tests","label":"K 测试"},{"id":"health","label":"F 健康"},{"id":"prd","label":"H 需求"}]}];
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'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)}
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>';}
    if(hh.related){h+='<h4>关联模块</h4><p>'+esc(hh.related)+'</p>';}
  }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||'')+(a.proxied_to?' <span style=color:var(--dim)>→ '+esc(a.proxied_to)+'</span>':'')+'</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.tests){h+='<h4>测试用例</h4><ul>';ai.tests.forEach(function(t){h+='<li><code>'+esc(t.id)+'</code> '+esc(t.name)+'</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>';}
    if(ai.related_modules&&ai.related_modules.length){h+='<h4>关联模块</h4><ul>';ai.related_modules.forEach(function(m){h+='<li><span class="help-btn" style="cursor:pointer;margin-right:6px" onclick="showModuleHelp(\''+esc(m.module)+'\',\'ai\')">§</span><code>'+esc(m.module)+'</code> — '+esc(m.relation)+'</li>'});h+='</ul>';}
  }
  box.innerHTML=h;
}
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 class="section"><div id="ct-'+c.id+'"><span class="dim">Loading...</span></div></div></div>';}
p+='</div>';}else{p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span class="dim">Loading...</span></div></div>';}}
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;}
function sw(id){if(id==='principles'){at='principles';as=localStorage.getItem('s')||'spec'}else{at=id;localStorage.setItem('t',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[3].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('s',id);document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].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}
var L=new Set();
async function fa(){try{var r=await fetch('/api/agents'),d=await r.json(),on=d.filter(function(a){return a.status==='online'}).length,er=d.reduce(function(s,a){return s+(a.errors||0)},0);
var h='<h2>Agents 总览<span class="help-btn" onclick="showModuleHelp(\'agents\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'agents\',\'ai\')" title="AI Spec">§</span></h2><div class="stats"><div class="stat-c g"><div class="n">'+on+'/'+d.length+'</div><div class="l">Agents Online</div></div><div class="stat-c '+(er>0?'y':'g')+'"><div class="n">'+(er||0)+'</div><div class="l">Errors</div></div><div class="stat-c g"><div class="n">'+d.length+'</div><div class="l">Total</div></div></div><div class="agents">';
for(var i=0;i<d.length;i++){var a=d[i];var st=a.status==='online'?'online':a.status==='degraded'?'degraded':'offline';var pb={windows:'w',linux:'l',mac:'m'}[a.platform]||'';
var rs=a.status_reason&&a.status_reason!=='OK'?'<div class="ar">'+esc(a.status_reason)+'</div>':'';
h+='<div class="ac '+st+'"><div class="ah" onclick="tgL(\''+a.id+'\')"><span class="sd '+(a.status==='online'?'g':a.status==='degraded'?'y':'r')+'"></span><div class="ai"><div class="an">'+esc(a.display_name||a.name)+(pb?' <span class="b '+pb+'">'+esc(a.platform)+'</span>':'')+'</div><div class="am">'+esc(a.jid||'')+(a.host?' &middot; '+esc(a.host):'')+rs+'</div></div><div class="as"><div class="v">'+(a.message_count_5min||0)+'</div><div class="l">msg/5m</div></div><div class="aa"><button class="btn s" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'start\')" '+(a.status!=='offline'||!a.restartable?'disabled':'')+'>Start</button><button class="btn x" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'stop\')" '+(a.status==='offline'||!a.restartable?'disabled':'')+'>Stop</button><button class="btn r" onclick="event.stopPropagation();doAct(\''+a.id+'\',\'restart\')" '+(!a.restartable?'disabled':'')+'>Restart</button><button class="btn l" onclick="event.stopPropagation();tgL(\''+a.id+'\')">Logs</button></div></div><div class="svcs">';
if(a.services){for(var k=0;k<a.services.length;k++){var s=a.services[k];h+='<span class="st"><span class="d '+(s.status||'stopped')+'"></span>'+esc(s.type)+(s.port?':<span class="p">'+s.port+'</span>':'')+'</span>';}}
h+='</div><div class="lp'+(L.has(a.id)?' open':'')+'" id="lp-'+a.id+'"><div class="lh"><span>Logs</span><button class="btn l" onclick="event.stopPropagation();fL(\''+a.id+'\')">Refresh</button></div><div class="lc" id="lc-'+a.id+'">Click Logs to load</div></div></div>';}
h+='</div>';qs('ct-agents').innerHTML=h;}catch(e){}}
async function doAct(id,act){try{var r=await fetch('/api/agents/'+id+'/'+act,{method:'POST'}),d=await r.json();toast(act+(d.ok?' OK':' FAILED'),d.ok?'ok':'err');setTimeout(fa,1500)}catch(e){toast('Error: '+e.message,'err')}}
function tgL(id){if(L.has(id))L.delete(id);else L.add(id);var lp=qs('lp-'+id);if(lp)lp.className='lp'+(L.has(id)?' open':'');if(L.has(id))fL(id)}
async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=await fetch('/api/agents/'+id+'/logs?lines=50'),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return'<div>'+esc(l)+'</div>'}).join('')||'<span class="dim">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span class="red">Failed</span>'}}
async function fHealth(){try{
  // Helpers
  function isHealthy(st){return st==='running'||st==='online'||(st&&st.indexOf('remote')===0);}
  // Service metadata: label / layer / impact (per-instance key → metadata)
  var M={};
  M['agent-001:xmpp_bot']={n:'研发助理-XMPP Bot',l:'通信层',i:'小小莫（Windows）XMPP 消息收发中断',spec:'xmpp_bot'};
  M['agent-002:hermes_gateway']={n:'Hermes网关-莫荷',l:'AI网关',i:'莫荷智能体（246 Linux）不可用',spec:'hermes_gateway'};
  M['agent-003:xmpp_bot']={n:'本地推理-XMPP Bot',l:'通信层',i:'小果（Mac）XMPP 消息收发中断',spec:'xmpp_bot'};
  M['agent-003:omlx_server']={n:'OMLX推理服务(小果)',l:'AI网关',i:'小果（Mac）本地推理服务不可用',spec:'omlx_server'};
  M['agent-004:hermes_gateway']={n:'Hermes网关-知微',l:'AI网关',i:'知微智能体（246 Linux）不可用',spec:'hermes_gateway'};
  M.wechat_bridge={n:'莫荷微信桥接',l:'通信层',i:'微信消息收发中断',spec:'wechat_bridge'};
  M.article_processor={n:'文章抓取服务',l:'辅助服务',i:'微信长文章无法读取',spec:'article_processor'};
  // 兼容层：/api/services 返回的旧名称仍然映射到友好标签
  M.xmpp_bot={n:'XMPP消息收发',l:'通信层',i:'消息收发中断',spec:'xmpp_bot'};
  M.hermes_gateway={n:'Hermes网关',l:'AI网关',i:'Hermes Agent不可用',spec:'hermes_gateway'};
  M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用'};
  M.gateway_mohe={n:'Gateway-莫荷',l:'AI网关',i:'莫荷智能体不可用',spec:'hermes_gateway'};
  M.gateway_zhiwei={n:'Gateway-知微',l:'AI网关',i:'知微智能体不可用',spec:'hermes_gateway'};
  M.dashboard={n:'管理门户',l:'辅助服务',i:'Dashboard无法访问',spec:'dashboard'};
  M.watchdog={n:'进程看门狗',l:'辅助服务',i:'异常进程无自动恢复'};
  M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用',spec:'ejabberd'};
  M['agents-health-check']={n:'健康检查(5min)',l:'定时任务',i:'5分钟级健康检测未运行'};
  M['agents-daily-health']={n:'日报健康检查(每日)',l:'定时任务',i:'日报统计未运行'};
  var layerOrder=['通信层','AI网关','辅助服务','定时任务'];

  var h='<h2>F 系统健康<span class="help-btn" onclick="showModuleHelp(\'health\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'health\',\'ai\')" title="AI Spec">§</span></h2>';
  var all=await Promise.all([
    fetch('/api/services').then(function(r){return r.json()}).catch(function(){return{}}),
    fetch('/api/expected').then(function(r){return r.json()}).catch(function(){return{}}),
    fetch('/api/monitor').then(function(r){return r.json()}).catch(function(){return{}})
  ]);
  var sD=all[0]||{},eD=all[1]||{},mD=all[2]||{};
  var svcs=sD.services||[],exp=eD.expected||[],act=eD.actual||{};

  // --- Layer 1: 系统概览 ---
  var hCount=svcs.filter(function(s){return s.health&&s.health.ok}).length;
  var cDown=exp.filter(function(e){return e.critical&&e.expected.indexOf('running')===0&&!isHealthy(act[e.name])}).length;
  var st=cDown>0?'critical':hCount<svcs.length?'degraded':'good';
  var stL={good:'正常',degraded:'部分异常',critical:'严重异常'};
  var stC={good:'var(--green)',degraded:'var(--yellow)',critical:'var(--red)'};
  h+='<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">';
  h+='<div style="background:'+stC[st]+';color:#000;border-radius:8px;padding:12px 20px;flex:1;min-width:160px;text-align:center"><div style=font-size:28px;font-weight:700>'+stL[st]+'</div><div style=font-size:17px;margin-top:4px;opacity:.8>'+hCount+'/'+svcs.length+' 服务健康</div></div>';
  h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+stC[st]+'>'+cDown+'</div><div style=font-size:17px;color:var(--dim)>关键异常</div></div>';
  if(mD.tier1&&mD.tier1.summary){var t1=mD.tier1.summary;var t1c=t1.ok===t1.total?'var(--green)':'var(--red)';
  h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+t1c+'>'+t1.ok+'/'+t1.total+'</div><div style=font-size:17px;color:var(--dim)>T1监控</div></div>';}
  h+='</div>';

  // --- Layer 2: 异常服务 ---
  var issues=[];
  for(var i=0;i<exp.length;i++){var e=exp[i];
    if(e.critical&&e.expected.indexOf('running')===0&&!isHealthy(act[e.name])){
      var ma=M[e.key||e.name]||{n:e.name,i:'未知影响'};
      issues.push({name:e.name,label:ma.n,impact:ma.i,status:act[e.name]||'stopped'});}}
  for(var i=0;i<svcs.length;i++){var s=svcs[i];
    if(!s.health||!s.health.ok){
      if(!issues.some(function(x){return x.name===s.name})){
        var m=M[s.name]||{n:s.name,i:'服务不可用'};
        issues.push({name:s.name,label:m.n,impact:m.i,status:'down'});}}}
  if(issues.length){
    h+='<div class=section><h2 style="color:var(--red);margin-bottom:8px">异常服务</h2>';
    for(var i=0;i<issues.length;i++){var iv=issues[i];
      h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:8px;padding:10px 14px;margin-bottom:6px">';
      h+='<div style=display:flex;align-items:center;gap:8px><span class="sd r"></span><strong>'+esc(iv.label)+'</strong><span class="tag r" style=font-size:18px>'+esc(iv.status)+'</span></div>';
      h+='<div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(iv.impact)+'</div></div>';}
    h+='</div>';}

  // --- Layer 3: 全部服务 (merge expected + services) ---
  var layers={};
  var added={};
  for(var i=0;i<exp.length;i++){var e=exp[i];
    if(e.check==='scheduled')continue; // 定时任务由下方单独区块渲染，此处跳过
    var ma=M[e.key||e.name]||{n:e.name,l:'辅助服务',i:'',spec:''};
    var ly=ma.l||'其他';
    if(!layers[ly])layers[ly]=[];
    added[e.name]=true;
    if(e.key) added[e.key]=true;  // 同时标记 key，防止 services 同名重复
    layers[ly].push({name:e.name,label:ma.n,port:e.port,status:isHealthy(act[e.name])?'running':act[e.name],critical:e.critical,spec:ma.spec||e.spec||''});}
  for(var i=0;i<svcs.length;i++){var s=svcs[i];
    if(!added[s.name]){
      var rawL=(M[s.name]||{}).l||(s.type==='infra'?'通信层':s.type==='gateway'?'AI网关':'辅助服务');
      var rawN=(M[s.name]||{}).n||s.name;
      if(!layers[rawL])layers[rawL]=[];
      var hStatus=s.health&&s.health.ok?'running':'down';
      var mSpec=(M[s.name]||{}).spec||'';
      layers[rawL].push({name:s.name,label:rawN,port:s.port,status:hStatus,critical:false,spec:mSpec});}}
  h+='<div class=section><h2 style=margin-bottom:8px>全部服务</h2>';
  for(var li=0;li<layerOrder.length;li++){var ln=layerOrder[li];
    var items=layers[ln];
    if(!items||!items.length)continue;
    h+='<div style=margin:8px 0 4px><strong style=font-size:19px;color:var(--accent)>'+esc(ln)+'</strong></div>';
    for(var j=0;j<items.length;j++){var it=items[j];
      var run=it.status==='running';
      h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;background:var(--bg);border-radius:4px;margin:2px 0">';
      h+='<span class="sd '+(run?'g':'r')+'"></span>';
      h+='<span style=flex:1>'+esc(it.label)+'</span>';
      if(it.port)h+='<span style=color:var(--dim);font-size:18px>:'+it.port+'</span>';
      if(it.spec){h+='<span class="help-btn" onclick="showModuleHelp(\''+it.spec+'\',\'human\')" title="使用说明">?</span>';
      h+='<span class="help-btn ai" onclick="showModuleHelp(\''+it.spec+'\',\'ai\')" title="AI Spec">§</span>';}
      h+='<span class="tag '+(run?'g':'r')+'">'+(run?'运行中':esc(it.status))+'</span>';
      h+='</div>';}
  }
  h+='</div>';

  // --- 定时任务状态 ---
  if(mD.tasks&&mD.tasks.length){
    h+='<div class=section><h2 style=margin-bottom:8px>定时任务</h2>';
    for(var i=0;i<mD.tasks.length;i++){var t=mD.tasks[i];
      var m=M[t.name]||{n:t.name};
      var tk=t.status==='ok'||t.status==='timer_ok'||t.status==='cron_ok'||t.status==='schtasks_ok';
      var tn=t.status==='not_deployed';
      h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;margin:2px 0">';
      h+='<span class="sd '+(tk?'g':tn?'y':'r')+'"></span>';
      h+='<span style=flex:1>'+esc(m.n)+'</span>';
      h+='<span class="tag '+(tk?'g':tn?'y':'r')+'">'+(tk?'正常':tn?'未部署':'异常')+'</span></div>';}
    h+='</div>';}

  // --- 自动修复记录 ---
  try{var ahR=await fetch('/api/autoheal');var ahD=await ahR.json();
  if(ahD&&ahD.time&&ahD.actions_taken&&ahD.actions_taken.length>0){
    h+='<div class=section><h2 style=margin-bottom:8px>自动修复记录 <span style=font-size:18px;color:var(--dim)>'+esc(ahD.time.slice(0,16).replace('T',' '))+'</span></h2>';
    for(var ai=0;ai<ahD.actions_taken.length;ai++){var a=ahD.actions_taken[ai];
      var isOK=a.detail&&a.detail.indexOf('OK')>=0;
      h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;margin:2px 0;background:var(--card);border-radius:4px">';
      h+='<span class="sd '+(isOK?'g':'r')+'"></span>';
      h+='<span style=flex:1>'+esc(a.action||'修复')+'</span>';
      h+='<span style=color:var(--dim);font-size:18px>'+esc(a.service||'')+'</span>';
      h+='<span class="tag '+(isOK?'g':'r')+'">'+(isOK?'成功':esc(a.detail||'失败'))+'</span></div>';}
    h+='</div>';}}
  catch(ahE){/* autoheal fetch failed, skip */}

  if(!h)h='<span class=dim>No data</span>';
  fill('health',h)}catch(e){fill('health','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json();var tasks=d.tasks||[];var s=document.getElementById('kf-status');var a=document.getElementById('kf-assignee');var fs=s?s.value:'';var fa=a?a.value:'';
if(tasks.length==0){fill('kanban','<h2>Kanban 看板<span class="help-btn" onclick="showModuleHelp(\'kanban\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'kanban\',\'ai\')" title="AI Spec">§</span></h2><div style=text-align:center;padding:40px;color:var(--dim)>No tasks</div>');return}
var html='<h2>Kanban 看板<span class="help-btn" onclick="showModuleHelp(\'kanban\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'kanban\',\'ai\')" title="AI Spec">§</span></h2><div style=display:flex;gap:8px;margin-bottom:16px>';html+='<select id=kf-status style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All Status</option><option value=ready>Ready</option><option value=progress>In Progress</option><option value=blocked>Blocked</option><option value=done>Done</option></select>';
html+='<select id=kf-assignee style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All</option><option value=mohe>mohe</option><option value=zhiwei>zhiwei</option><option value=xxm>xxm</option><option value=xiaoguo>xiaoguo</option></select>';
html+='<span style=color:var(--dim);font-size:18px;align-self:center>'+tasks.length+' tasks</span></div>';
var filtered=tasks;if(fs)filtered=filtered.filter(function(t){return t.status===fs});if(fa)filtered=filtered.filter(function(t){return t.assignee===fa});
var colors={ready:'var(--green)',progress:'var(--yellow)',blocked:'var(--red)',done:'var(--dim)'};var labels={ready:'Ready',progress:'In Progress',blocked:'Blocked',done:'Done'};
html+=filtered.map(function(t){return'<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;border-left:3px solid '+(colors[t.status]||'var(--dim)')+'" id=kc-'+t.id+'"><div style=font-size:18px;font-weight:600>'+esc(t.title)+'</div><div style=display:flex;gap:12px;font-size:19px;color:var(--dim);margin-bottom:6px>'
+'<span style="display:inline-block;padding:1px 6px;border-radius:3px;font-size:18px;font-weight:600;background:'+(colors[t.status]||'var(--dim)')+';color:#000">'+(labels[t.status]||t.status)+'</span>'
+'<span>'+(t.assignee?'@'+esc(t.assignee):'')+'</span>'
+'<span>'+(t.created_at?new Date(t.created_at*1000).toLocaleDateString():'')+'</span></div>'
+(t.body?'<div style=font-size:18px;color:var(--dim);white-space:pre-wrap;max-height:60px;overflow:hidden;cursor:pointer onclick="this.classList.toggle(\'expanded\');if(this.style.maxHeight!==\'none\')this.style.maxHeight=\'none\';else this.style.maxHeight=\'60px\'">'+esc(t.body)+'</div>':'')
+'</div>'}).join('');
fill('kanban',html);
var es=document.getElementById('kf-status');var ea=document.getElementById('kf-assignee');
if(es&&!s)es.value=fs;if(ea&&!a)ea.value=fa;
if(es)es.onchange=fK;if(ea)ea.onchange=fK;}catch(e){}}
async function fI(){
  try{
    var ci=document.getElementById('ct-infra');if(!ci)return;
    var ls=ci.querySelector('span.dim');if(ls)ls.remove();
    /* --- Stable wrapper for Platform+Ejabberd (rebuilt each cycle, no flicker since no state to preserve) --- */
    var pe=document.getElementById('infra-pe');
    if(!pe){pe=document.createElement('div');pe.id='infra-pe';ci.appendChild(pe);}
    var h='';
    try{var r=await fetch('/api/platform'),d=await r.json();
      h+='<div class="ps"><h2>Platform<span class="help-btn" onclick="showModuleHelp(\'infra\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'infra\',\'ai\')" title="AI Spec">§</span></h2><div class="psv">';
      for(var i=0;i<d.length;i++){var s=d[i];h+='<div class="i"><span class="d '+(s.status==='running'?'ok':'stopped')+'"></span><span class="sn">'+esc(s.name)+'</span><span class="help-btn" onclick="showModuleHelp(\''+s.id+'\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\''+s.id+'\',\'ai\')" title="AI Spec">§</span></div>';}
      try{var r2=await fetch('/api/ejabberd'),ej=await r2.json();var ejA=ej.alive||ej.xmpp_bot_connected;
        h+='<div class="i"><span class="d '+(ejA?'ok':'stopped')+'"></span><span class="sn">Ejabberd XMPP</span><span class="help-btn" onclick="showModuleHelp(\'ejabberd\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'ejabberd\',\'ai\')" title="AI Spec">§</span></div>';
      }catch(ejErr){}
      h+='</div></div>';
    }catch(e){}
    pe.innerHTML=h;

    /* --- EasyTier section: create once, update state each cycle (no flicker) --- */
    var et=document.getElementById('easytier-section');
    if(!et){
      et=document.createElement('div');et.id='easytier-section';et.className='ps';et.style.marginTop='16px';
      et.innerHTML='<h2>EasyTier VPN<span class="help-btn" onclick="showModuleHelp(\'easytier\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'easytier\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=et-w></span>Windows <span id=et-w-ip></span></div><div class=i><span class="d" id=et-246></span>246 <span id=et-246-ip></span></div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-et-on>Turn On</button><button class="btn x" id=btn-et-off>Turn Off</button><span id=et-status style="font-size:12px;color:var(--dim)">-</span></div>';
      ci.appendChild(et);
      document.getElementById('btn-et-on').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
      document.getElementById('btn-et-off').onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('ET Fail','err')})};
    }
    try{var e3=await fetch('/api/easytier').then(function(r){return r.json()});var s=e3.status||{};
      var e1=document.getElementById('et-w');if(e1)e1.className='d '+(s.windows==='running'?'ok':'stopped');
      var e2=document.getElementById('et-246');if(e2)e2.className='d '+(s['246']==='running'?'ok':'stopped');
      if(e3.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=e3.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=e3.virtual_ips['246']||'';}
      var allRun=Object.values(s).every(function(v){return v==='running'||v==='pending'});
      document.getElementById('btn-et-on').disabled=allRun;
      document.getElementById('btn-et-off').disabled=!allRun;
      document.getElementById('et-status').textContent=allRun?'Connected':'Off';
    }catch(e3){}

    /* --- RDP section: create once, update state each cycle (no flicker) --- */
    var rd=document.getElementById('rdp-section');
    if(!rd){
      rd=document.createElement('div');rd.id='rdp-section';rd.className='ps';rd.style.marginTop='16px';
      rd.innerHTML='<h2>RDP Remote Desktop<span class="help-btn" onclick="showModuleHelp(\'rdp\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'rdp\',\'ai\')" title="AI Spec">§</span></h2><div class=psv><div class=i><span class="d" id=rdp-st></span>RDP <span id=rdp-info></span></div><div class=i><span class="d" id=rdp-tun></span>Tunnel</div></div><div style="margin-top:8px;display:flex;gap:8px;align-items:center"><button class="btn s" id=btn-rdp-on>Enable</button><button class="btn x" id=btn-rdp-off>Disable</button><span id=rdp-status-txt style="font-size:12px;color:var(--dim)">-</span></div>';
      ci.appendChild(rd);
      document.getElementById('btn-rdp-on').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){fI()}).catch(function(){toast('RDP Fail','err')})};
      document.getElementById('btn-rdp-off').onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){fI()}).catch(function(){toast('RDP Fail','err')})};
    }
    try{var e4=await fetch('/api/rdp').then(function(r){return r.json()});
      document.getElementById('rdp-st').className='d '+(e4.rdp_enabled?'ok':'stopped');
      document.getElementById('rdp-tun').className='d '+(e4.tunnel_running?'ok':'stopped');
      document.getElementById('rdp-info').textContent=e4.public_endpoint||'';
      var fOn=e4.rdp_enabled&&e4.tunnel_running;
      document.getElementById('btn-rdp-on').disabled=fOn;
      document.getElementById('btn-rdp-off').disabled=!fOn;
document.getElementById('rdp-status-txt').textContent=fOn?'Connected':e4.rdp_enabled?'Tunnel pending':'Disconnected';
    }catch(e4){}

    /* --- OpenCode Go Usage section: create once, update state each cycle (no flicker) --- */
    var us=document.getElementById('usage-section');
    if(!us){
      us=document.createElement('div');us.id='usage-section';us.className='ps';us.style.marginTop='16px';
      us.innerHTML='<h2>OpenCode Go Usage<span class="help-btn" onclick="showModuleHelp(\'usage_monitor\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'usage_monitor\',\'ai\')" title="AI Spec">§</span></h2>'
        +'<div id="usage-cards" style="display:flex;flex-wrap:wrap;gap:10px;"></div>'
        +'<div style="margin-top:8px;display:flex;gap:8px;align-items:center">'
        +'<button class="btn s" id="btn-usage-refresh">Refresh Now</button>'
        +'<span id="usage-last" style="font-size:12px;color:var(--dim)">-</span>'
        +'</div>';
      ci.appendChild(us);
      document.getElementById('btn-usage-refresh').onclick=function(){
        var btn=this;btn.disabled=true;
        document.getElementById('usage-last').textContent='采集中…';
        fetch('/api/usage/refresh',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
          .then(function(r){return r.json()})
          .then(function(d){
            if(d.ok){toast('采集已触发，10-15s 后自动刷新');}
            else{toast('Refresh 失败: '+(d.error||'unknown'),'err');}
            setTimeout(function(){btn.disabled=false;fI()},8000);
          })
          .catch(function(e){toast('Network error','err');btn.disabled=false;});
      };
    }
    try{
      var e5=await fetch('/api/usage').then(function(r){return r.json()});
      var cards=document.getElementById('usage-cards');
      var lu=document.getElementById('usage-last');
      if(!e5||!e5.ok){
        if(lu)lu.textContent='—'+(e5&&e5.error?' '+e5.error:'');
        if(cards)cards.innerHTML='<div style="color:var(--dim);padding:8px">'+esc(e5&&e5.error||'未采集')+'</span>';
      }else{
        // Update last refresh time
        if(lu){
          var ts=e5.last_refresh_iso;
          var d = new Date(ts);
          var pad=function(n){return String(n).padStart(2,'0')};
          lu.textContent='上次采集: '+d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())
            +' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds())+' CST';
        }
        // Render cards via innerHTML rebuild (simple data cards, no flicker risk)
        var html='';
        var accts=e5.accounts||[];
        for(var i=0;i<accts.length;i++){
          var a=accts[i];
          var maxPct=0;
          if(a.rolling&&a.rolling.usage_percent!=null)maxPct=Math.max(maxPct,a.rolling.usage_percent);
          if(a.weekly&&a.weekly.usage_percent!=null)maxPct=Math.max(maxPct,a.weekly.usage_percent);
          if(a.monthly&&a.monthly.usage_percent!=null)maxPct=Math.max(maxPct,a.monthly.usage_percent);
          var edge='var(--border)';
          var barColor='var(--accent)';
          if(maxPct>=95){edge='#e53935';barColor='#e53935';}
          else if(maxPct>=80){edge='#fb8c00';barColor='#fb8c00';}
          html+='<div style="background:var(--card);border:1px solid '+edge+';border-radius:8px;padding:10px 12px;min-width:200px;flex:1;max-width:260px">'
            +'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">'
            +'<strong style="font-size:14px">'+esc(a.label||a.key_id||a.workspace_id||'?')+'</strong>';
          if(a.session_expired){
            html+='<span style="font-size:11px;color:#e53935;background:rgba(229,57,53,.1);padding:2px 6px;border-radius:4px">SESSION EXPIRED</span>';
          }else if(a.error){
            html+='<span style="font-size:11px;color:var(--dim)" title="'+esc(a.error)+'">⚠</span>';
          }else{
            html+='<span style="font-size:11px;color:var(--dim)">'+maxPct+'% max</span>';
          }
          html+='</div>';
          if(a.session_expired||a.error){
            html+='<div style="font-size:12px;color:var(--dim);padding:4px 0">'+esc(a.error||'请重新登录此账号')+'</div>';
          }else{
            html+=_renderUsageMetric('5h rolling',a.rolling,barColor);
            html+=_renderUsageMetric('Weekly',a.weekly,barColor);
            html+=_renderUsageMetric('Monthly',a.monthly,barColor);
          }
          html+='</div>';
        }
        if(!accts.length){html='<div style="color:var(--dim);padding:8px">未配置账号 (accounts.json 未就绪，或 cookies 未提取)</div>';}
        if(cards)cards.innerHTML=html;
      }
    }catch(e5){}
  }catch(e){}
}
function _fmtCountdown(sec){
  if(sec==null)return '—';
  if(sec<=0)return '即将重置';
  sec=Math.floor(sec);
  var d=Math.floor(sec/86400),h=Math.floor((sec%86400)/3600),m=Math.floor((sec%3600)/60),s=sec%60;
  if(d>0)return d+'d '+h+'h';
  if(h>0)return h+'h '+m+'m';
  if(m>0)return m+'m '+s+'s';
  return s+'s';
}
function _renderUsageMetric(label,m,barColor){
  if(!m||m.usage_percent==null){
    return '<div style="margin-bottom:6px"><div style="font-size:11px;color:var(--dim);display:flex;justify-content:space-between"><span>'+esc(label)+'</span></div><div style="background:var(--bg);border-radius:4px;height:6px;margin-top:2px"></div><div style="font-size:10px;color:var(--dim);margin-top:1px">—</div></div>';
  }
  var pct=m.usage_percent;
  var txt=pct+'% · '+_fmtCountdown(m.reset_in_sec);
  return '<div style="margin-bottom:6px">'
    +'<div style="font-size:11px;color:var(--dim);display:flex;justify-content:space-between"><span>'+esc(label)+'</span><span>'+(pct>=95?'CRITICAL':pct>=80?'WARNING':'OK')+'</span></div>'
    +'<div style="background:var(--bg);border-radius:4px;height:6px;margin-top:2px;overflow:hidden">'
    +'<div style="background:'+barColor+';height:100%;width:'+pct+'%;transition:width .3s"></div>'
    +'</div>'
    +'<div style="font-size:10px;color:var(--dim);margin-top:1px">'+esc(txt)+'</div>'
    +'</div>';
}
async function fH(){try{
  var r=await fetch('/api/prd'),d=await r.json();
  if(!d.ok||!d.content){fill('prd','Failed');return}
  var h='<h2>H 产品需求<span class="help-btn" onclick="showModuleHelp(\'prd\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'prd\',\'ai\')" title="AI Spec">§</span></h2><div style=padding:8px 0>'+mdRender(d.content)+'</div>';
  try{var rh=await fetch('/api/prd/history'),dh=await rh.json();
    if(dh.ok&&dh.log&&dh.log.length){
      h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'prd-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=prd-history style=display:none;margin-top:8px>';
      for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
      h+='</div></div>';}
  }catch(e){}
  fill('prd',h)}catch(e){fill('prd','Error')}}

async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='<h2>K 测试报告<span class="help-btn" onclick="showModuleHelp(\'tests\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'tests\',\'ai\')" title="AI Spec">§</span></h2>';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
var s=d.summary;h+='<div style=display:flex;gap:12px;margin-bottom:16px><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--accent)>'+s.total+'</div><div style=color:var(--dim);font-size:19px>Total</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--green)>'+s.passed+'</div><div style=color:var(--dim);font-size:19px>Passed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--red)>'+s.failed+'</div><div style=color:var(--dim);font-size:19px>Failed</div></div></div>';
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--yellow)>Expected Fail ('+s.failed+')</div>';
d.tests.filter(function(t){return!t.ok&&t.expected}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--yellow);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--yellow);color:#000;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">EXPECTED</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
var realFails=d.tests.filter(function(t){return!t.ok&&!t.expected});
if(realFails.length){h+='<div style=margin-bottom:8px;font-weight:600;color:var(--red)>Real Fail ('+realFails.length+')</div>';realFails.forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:6px;padding:8px 12px;margin-bottom:6px"><div style=display:flex;align-items:center;gap:8px><span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:19px">FAIL</span><strong>'+esc(t.name)+'</strong></div><div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(t.detail||'')+'</div></div>';});}
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--green)>Passed ('+s.passed+')</div>';
d.tests.filter(function(t){return t.ok}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--green);color:#fff;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">PASS</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
h+='<div style=margin-top:8px;color:var(--dim);font-size:19px>Last run: '+esc(d.time||'')+'</div>';
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}

var FETCHES={agents:fa,spec:fS2,tests:fTests,health:fHealth,prd:fH,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();var skip=['spec','prd','health'];await Promise.all(Object.keys(FETCHES).filter(function(k){return skip.indexOf(k)<0}).map(function(k){return FETCHES[k]()}));}

async function fE2(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var e1=document.getElementById('et-w');if(e1){e1.className='d '+(s.windows==='running'?'ok':'stopped')}var e2=document.getElementById('et-246');if(e2){e2.className='d '+(s['246']==='running'?'ok':'stopped')}if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}}catch(e){}}
async function fR(){try{var r=await fetch('/api/rdp'),d=await r.json();var e1=document.getElementById('rdp-st');if(e1){e1.className='d '+(d.rdp_enabled?'ok':'stopped')}var e2=document.getElementById('rdp-tun');if(e2){e2.className='d '+(d.tunnel_running?'ok':'stopped')}var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||''}catch(e){}}
var fS2_c=null;var fS2_last='';async function fS2(){try{
  var r=await fetch('/api/spec'),d=await r.json();
  if(!d.ok||!d.content){fill('spec','Failed');return}
  var h='<h2>G 开发规范<span class="help-btn" onclick="showModuleHelp(\'dev_spec\',\'human\')" title="使用说明">?</span><span class="help-btn ai" onclick="showModuleHelp(\'dev_spec\',\'ai\')" title="AI Spec">§</span></h2><div style=padding:8px 0>'+mdRender(d.content)+'</div>';
  try{var rh=await fetch('/api/spec/history'),dh=await rh.json();
    if(dh.ok&&dh.log&&dh.log.length){
      h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'spec-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=spec-history style=display:none;margin-top:8px>';
      for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
      h+='</div></div>';}
  }catch(e){}
  fill('spec',h)}catch(e){fill('spec','Error')}}

function mdRender(md){if(!md)return'';var h='';var blocks=md.split('\n\n');for(var b=0;b<blocks.length;b++){var block=blocks[b].trim();if(!block)continue;
if(block.startsWith('```')){var code=block.replace(/^```[\s\S]*?\n/,'').replace(/```$/,'').trim();h+='<pre class=mono style=background:var(--bg);padding:12px;border-radius:6px;overflow-x:auto>'+esc(code)+'</pre>';continue;}
if(block.startsWith('### ')){var txt=block.slice(4);var anno=txt.match(/<!--(.*?)-->/);if(anno){var status=anno[1].trim();txt=txt.replace(/<!--.*?-->/,'').trim();var badge=status.indexOf('✅')>=0?'<span class=tag g style=margin-left:8px>'+esc(status)+'</span>':status.indexOf('🔄')>=0?'<span class=tag y style=margin-left:8px>'+esc(status)+'</span>':'<span class=tag r style=margin-left:8px>'+esc(status)+'</span>';h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:19px>'+esc(txt)+badge+'</h3>'}else{h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:19px>'+esc(txt)+'</h3>'}continue;}
if(block.startsWith('## ')){var txt=block.slice(3);var anno=txt.match(/<!--(.*?)-->/);if(anno){var status=anno[1].trim();txt=txt.replace(/<!--.*?-->/,'').trim();var badge=status.indexOf('✅')>=0?'<span class=tag g style=margin-left:8px>'+esc(status)+'</span>':status.indexOf('🔄')>=0?'<span class=tag y style=margin-left:8px>'+esc(status)+'</span>':'<span class=tag r style=margin-left:8px>'+esc(status)+'</span>';h+='<h2 style=color:var(--accent);margin:20px 0 8px;font-size:19px;border-bottom:1px solid var(--border);padding-bottom:4px>'+esc(txt)+badge+'</h2>'}else{h+='<h2 style=color:var(--accent);margin:20px 0 8px;font-size:19px;border-bottom:1px solid var(--border);padding-bottom:4px>'+esc(block.slice(3))+'</h2>'}continue;}
if(block.startsWith('# ')){h+='<h1 style=color:var(--accent);margin:24px 0 10px;font-size:24px>'+esc(block.slice(2))+'</h1>';continue;}
if(block.startsWith('> ')){h+='<blockquote style="border-left:3px solid var(--accent);padding:8px 16px;margin:8px 0;color:var(--dim);background:var(--bg);border-radius:4px">'+esc(block.slice(2))+'</blockquote>';continue;}
if(block.startsWith('---')||block.startsWith('***')){h+='<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';continue;}
if(block.startsWith('- ')||block.startsWith('* ')){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^[-*]\s*/,'').trim();if(li){if(!inList){h+='<ul style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ul>';continue;}
if(/^\d+\.\s/.test(block)){var items=block.split('\n'),inList=false;for(var i=0;i<items.length;i++){var li=items[i].replace(/^\d+\.\s*/,'').trim();if(li){if(!inList){h+='<ol style=margin:4px 0;padding-left:20px>';inList=true}h+='<li style=margin:2px 0>'+inMd(li)+'</li>'}}if(inList)h+='</ol>';continue;}
if(block.indexOf('|')>=0&&block.indexOf('\n|')>=0){var rows=block.split('\n'),inTable=false;for(var i=0;i<rows.length;i++){if(rows[i].indexOf('---')>=0)continue;var cells=rows[i].split('|').filter(function(c){return c.trim()});if(cells.length<2)continue;if(!inTable){h+='<table style=margin:8px 0;font-size:18px><tr>';for(var c=0;c<cells.length;c++){h+='<th style="padding:4px 8px;border:1px solid var(--border);color:var(--dim);font-weight:600;background:var(--bg)">'+inMd(cells[c].trim())+'</th>'}h+='</tr>';inTable=true}else{h+='<tr>';for(var c=0;c<cells.length;c++){h+='<td style="padding:4px 8px;border:1px solid var(--border)">'+inMd(cells[c].trim())+'</td>'}h+='</tr>'}}if(inTable)h+='</table>';continue;}
h+='<p style="line-height:1.7;margin:6px 0">'+inMd(block)+'</p>';}
return h;
function inMd(t){return t.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent)" target=_blank>$1</a>')}}
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
</script></body></html>