Files
AgentsMeeting/gateway/scripts/templates/dashboard.html
T
hmo a3ff5062c5 文档checklist: dev-spec.md标注实现状态
- dev-spec.md: 每节标题加<!-- /🔄/ -->状态标注
- 表格新增 状态 列, 逐条标记实现情况
- mdRender: 解析HTML注释渲染为彩色标签
- scrollTop保存修复文档阅读位置
2026-07-14 02:49:08 +08:00

131 lines
28 KiB
HTML

<!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:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:24px}
h1{font-size:20px;font-weight:600;color:var(--accent);margin-bottom:4px}.subtitle{color:var(--dim);font-size:12px;margin-bottom:20px}
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.tab-btn{padding:10px 20px;cursor:pointer;font-size:13px;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:28px;font-weight:700}.stat-c .l{font-size:11px;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:15px;font-weight:600}
.am{font-size:12px;color:var(--dim);margin-top:2px}
.b{display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;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:13px;font-weight:600}.as .l{font-size:10px;color:var(--dim)}
.aa{display:flex;gap:4px}
.btn{padding:5px 12px;border:1px solid var(--border);border-radius:5px;background:#21262d;color:var(--text);cursor:pointer;font-size:11px}
.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:3px 10px;border-radius:12px;font-size:11px;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:10px;margin-left:4px}
.lp{display:none;border-top:1px solid var(--border);padding:12px 16px;background:#0a0e14}
.lp.open{display:block}.lh{font-size:12px;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}
.toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:6px;font-size:13px;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:16px;color:var(--dim);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.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:12px}
.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:15px;font-weight:600;color:var(--accent)}
.mono{font-family:"Cascadia Code",Consolas,monospace;font-size:12px}
.tag{display:inline-block;padding:1px 8px;border-radius:8px;font-size:11px;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:12px}th,td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)}
th{color:var(--dim);font-weight:600;font-size:11px}
.log-line{font:11px/1.6 "Cascadia Code",Consolas,monospace;color:var(--dim);padding:1px 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:12px;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}}
</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>
<script>
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"},{"id":"spec","label":"G 规范"},{"id":"prd","label":"H 需求"}]}];
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'git';
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)}
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')||'git'}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='<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]||'';
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):'')+'</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 fG(){try{var r=await fetch('/api/git'),d=await r.json();var h='';for(var i=0;i<d.log.length;i++){h+='<div class="log-line">'+esc(d.log[i])+'</div>';}if(!h)h='<span class="dim">No commits</span>';fill('git',h)}catch(e){}}
async function fS(){try{var r=await fetch('/api/services'),d=await r.json(),ok=0;var h='<table><tr><th>Service</th><th>Port</th><th>Health</th><th>WD</th><th>Type</th></tr>';for(var i=0;i<d.services.length;i++){var s=d.services[i],a=s.health&&s.health.ok;if(a)ok++;h+='<tr><td>'+esc(s.name)+'</td><td>'+(s.port||'-')+'</td><td>'+(a?'OK':'DOWN')+'</td><td>'+(s.watchdog?'Y':'N')+'</td><td>'+esc(s.type)+'</td></tr>';}h+='</table>';fill('svc',h)}catch(e){}}
async function fM(){try{var r=await fetch('/api/monitor'),d=await r.json();var h='';if(d.tier1)h+='<div>T1: '+d.tier1.summary.ok+'/'+d.tier1.summary.total+' ok</div>';if(d.tier2)h+='<div>T2: '+d.tier2.summary.ok+'/'+d.tier2.summary.total+' ok</div>';fill('mon',h)}catch(e){}}
async function fT(){try{var r=await fetch('/api/todos'),d=await r.json();fill('todos','<span>'+(d.todos.length)+' records</span>')}catch(e){}}
async function fE(){try{var r=await fetch('/api/metagrowth'),d=await r.json();fill('meta','fixes: '+(d.total_fixes||0)+' commits: '+(d.commit_count||0))}catch(e){}}
async function fF(){try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}fill('exp',ok+'/'+d.expected.length+' compliant')}catch(e){}}
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json(),n=(d.tasks||[]).length;fill('kanban',n?'<span>'+n+' tasks</span>':'<span class="dim">No tasks</span>')}catch(e){}}
async function fI(){try{ var r=await fetch('/api/platform'),d=await r.json(); var h='<div class="ps"><h2>Platform</h2><div class="psv">'; for(var i=0;i<d.length;i++){h+='<div class="i"><span class="d '+(d[i].status==='running'?'ok':'stopped')+'"></span>'+esc(d[i].name)+'</div>';} h+='</div></div>'; try{var r2=await fetch('/api/ejabberd'),e=await r2.json();var ea=e.alive||e.xmpp_bot_connected; h+='<div class="ps"><h2>Ejabberd</h2><div class="psv"><div class="i"><span class="d '+(ea?'ok':'stopped')+'"></span>'+(ea?'ALIVE':'DOWN')+'</div></div></div>';}catch(e2){} fill('infra',h); /* EasyTier section */ try{var r3=await fetch('/api/easytier'),e3=await r3.json(),s=e3.status||{}; var et=document.createElement('div');et.className='ps';et.style.marginTop='16px'; var av=(e3.virtual_ips||{});var wip=av.windows||'';var tip=av['246']||''; et.innerHTML='<h2>EasyTier VPN</h2><div class=psv><div class=i><span class=d id=et-w></span>Windows <span id=et-w-ip>'+wip+'</span></div><div class=i><span class=d id=et-246></span>246 <span id=et-246-ip>'+tip+'</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>'; var ci=document.getElementById('ct-infra');if(ci)ci.appendChild(et); 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'); var allRun=Object.values(s).every(function(v){return v==='running'||v==='pending'}); var ob=document.getElementById('btn-et-on');var ofb=document.getElementById('btn-et-off');var stx=document.getElementById('et-status'); if(ob){ob.disabled=allRun;ob.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('ET Fail','err')})};} if(ofb){ofb.disabled=!allRun;ofb.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('ET Fail','err')})};} if(stx)stx.textContent=allRun?'Connected':'Off';}catch(e3){} /* RDP section */ try{var r4=await fetch('/api/rdp'),e4=await r4.json(); var rd=document.createElement('div');rd.className='ps';rd.style.marginTop='16px'; rd.innerHTML='<h2>RDP Remote Desktop</h2><div class=psv><div class=i><span class=d id=rdp-st></span>RDP <span id=rdp-info>'+esc(e4.public_endpoint||'')+'</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>'; var ci2=document.getElementById('ct-infra');if(ci2)ci2.appendChild(rd); var ee1=document.getElementById('rdp-st');if(ee1)ee1.className='d '+(e4.rdp_enabled?'ok':'stopped'); var ee2=document.getElementById('rdp-tun');if(ee2)ee2.className='d '+(e4.tunnel_running?'ok':'stopped'); var fOn=e4.rdp_enabled&&e4.tunnel_running; var ob2=document.getElementById('btn-rdp-on');var ofb2=document.getElementById('btn-rdp-off');var stx2=document.getElementById('rdp-status-txt'); if(ob2){ob2.disabled=fOn;ob2.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('RDP Fail','err')})};} if(ofb2){ofb2.disabled=!fOn;ofb2.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fI,500)}).catch(function(e){toast('RDP Fail','err')})};} if(stx2)stx2.textContent=fOn?'Connected':e4.rdp_enabled?'Tunnel pending':'Disconnected';}catch(e4){} }catch(e){}}
async function fH(){try{var r=await fetch('/api/prd'),d=await r.json();if(!d.ok||!d.content){fill('prd','Failed');return}var el=document.getElementById('ct-prd');var sp=el?el.querySelector('div')?el.querySelector('div').scrollTop:0:0;fill('prd','<div style=max-height:600px;overflow-y:auto;background:var(--bg);padding:16px;border-radius:6px>'+mdRender(d.content)+'</div>');var el2=document.getElementById('ct-prd');if(el2&&sp){setTimeout(function(){(el2.querySelector('div')||el2).scrollTop=sp},0)}}catch(e){fill('prd','Error')}}var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,spec:fS2,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();await Promise.all(Object.keys(FETCHES).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;async function fS2(){try{var r=await fetch('/api/spec'),d=await r.json();if(!d.ok||!d.content){fill('spec','Failed');return}var el=document.getElementById('ct-spec');var sp=el?el.querySelector('div')?el.querySelector('div').scrollTop:0:0;fill('spec','<div style=max-height:600px;overflow-y:auto;background:var(--bg);padding:16px;border-radius:6px>'+mdRender(d.content)+'</div>');var el2=document.getElementById('ct-spec');if(el2&&sp){setTimeout(function(){(el2.querySelector('div')||el2).scrollTop=sp},0)}}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:15px>'+esc(txt)+badge+'</h3>'}else{h+='<h3 style=color:var(--accent);margin:16px 0 6px;font-size:15px>'+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:17px;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:17px;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:20px>'+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:12px><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);
async function fetchEasyTierStatus(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var c=document.getElementById('ct-infra');if(!c)return;var el=document.getElementById('easytier-section');if(!el){el=document.createElement('div');el.id='easytier-section';el.innerHTML='<div class=ps style=margin-top:16px><h2>EasyTier VPN</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 disabled>Turn Off</button><span id=et-status style=font-size:12px;color:var(--dim)>-</span></div></div>';c.appendChild(el);var ob=document.getElementById('btn-et-on');var ofb=document.getElementById('btn-et-off');if(ob)ob.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fetchEasyTierStatus,2000);toast('EasyTier On OK')}).catch(function(e){toast('EasyTier On FAIL','err')})};if(ofb)ofb.onclick=function(){fetch('/api/easytier/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fetchEasyTierStatus,2000);toast('EasyTier Off OK')}).catch(function(e){toast('EasyTier Off FAIL','err')})};}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']||''}var allRunning=Object.values(s).every(function(v){return v==='running'||v==='pending'});var onBtn=document.getElementById('btn-et-on');var offBtn=document.getElementById('btn-et-off');var stxt=document.getElementById('et-status');if(onBtn)onBtn.disabled=allRunning;if(offBtn)offBtn.disabled=!allRunning;if(stxt)stxt.textContent=allRunning?'Connected':'Off'}catch(e){}}
async function fetchRdpStatus(){try{var r=await fetch('/api/rdp'),d=await r.json();var c=document.getElementById('ct-infra');if(!c)return;var el=document.getElementById('rdp-section');if(!el){el=document.createElement('div');el.id='rdp-section';el.innerHTML='<div class=ps style=margin-top:16px><h2>RDP Remote Desktop</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 disabled>Disable</button><span id=rdp-status-txt style=font-size:12px;color:var(--dim)>-</span></div></div>';c.appendChild(el);var ob=document.getElementById('btn-rdp-on');var ofb=document.getElementById('btn-rdp-off');if(ob)ob.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'start'})}).then(function(){setTimeout(fetchRdpStatus,2000);toast('RDP On OK')}).catch(function(e){toast('RDP On FAIL','err')})};if(ofb)ofb.onclick=function(){fetch('/api/rdp/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'stop'})}).then(function(){setTimeout(fetchRdpStatus,2000);toast('RDP Off OK')}).catch(function(e){toast('RDP Off FAIL','err')})};}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||'';var fullyOn=d.rdp_enabled&&d.tunnel_running;var onBtn=document.getElementById('btn-rdp-on');var offBtn=document.getElementById('btn-rdp-off');var stxt=document.getElementById('rdp-status-txt');if(onBtn)onBtn.disabled=fullyOn;if(offBtn)offBtn.disabled=!fullyOn;if(stxt)stxt.textContent=fullyOn?'Connected':d.rdp_enabled?'Tunnel pending':'Disconnected'}catch(e){}}
fetchEasyTierStatus();fetchRdpStatus();setInterval(fetchEasyTierStatus,10000);setInterval(fetchRdpStatus,10000);setInterval(fE,10000);setInterval(fR,10000);
</script></body></html>