166 lines
26 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>曜冥纪元 · 领地健康看板</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#080c14;--s1:#0c1320;--s2:#101828;--s3:#162032;--cyan:#5ad0e0;--cyan-glow:rgba(90,208,224,.3);--violet:#b090ff;--violet-glow:rgba(176,144,255,.25);--ok:#40b87a;--ok-glow:rgba(64,184,122,.3);--warn:#f0d060;--err:#ff4a6e;--brd:rgba(90,208,224,.12);--brd-hover:rgba(90,208,224,.3);--txt:#f0f6ff;--dim:#6a80a0;--font:'SF Pro Display','Inter','PingFang SC','-apple-system',sans-serif;--mono:'SF Mono','JetBrains Mono',monospace}
@keyframes breathe-dot{0%,100%{opacity:.5;transform:scale(1)}50%{opacity:1;transform:scale(1.2)}}
@keyframes breathe-card{0%,100%{box-shadow:0 0 0 rgba(90,208,224,0)}50%{box-shadow:0 0 18px rgba(90,208,224,.06),0 0 40px rgba(123,93,191,.03)}}
@keyframes shimmer{0%{transform:translateX(-100%)}50%{transform:translateX(100%)}100%{transform:translateX(200%)}}
@keyframes border-flow{to{background-position:0 0,-300% 0}}
html{font-size:15px}
body{background:var(--bg);color:var(--txt);font-family:var(--font);min-height:100vh;overflow-x:hidden;-webkit-font-smoothing:antialiased}
.hd{background:rgba(8,12,20,.92);border-bottom:1px solid var(--brd);padding:22px 48px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:50}
.hd-l{display:flex;align-items:center;gap:16px}
.hd-logo{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--cyan),var(--violet));box-shadow:0 0 30px rgba(90,208,224,.3);flex-shrink:0}
.hd-tt{font-size:20px;font-weight:700;letter-spacing:.08em}
.hd-st{font-size:12px;color:var(--dim);margin-top:3px;letter-spacing:.04em}
.hd-r{text-align:right}
.hd-tm{font-size:20px;color:var(--cyan);font-family:var(--mono);letter-spacing:2px;font-weight:600}
.hd-ss{font-size:12px;margin-top:4px;display:flex;align-items:center;gap:8px;justify-content:flex-end}
.sm{display:flex;gap:14px;padding:18px 48px;background:rgba(12,19,32,.6);border-bottom:1px solid var(--brd);overflow-x:auto}
.sm-i{background:linear-gradient(var(--s2),var(--s2)) padding-box,linear-gradient(90deg,var(--cyan),var(--violet),var(--cyan)) border-box;border:2px solid transparent;border-radius:12px;padding:12px 22px;flex-shrink:0;min-width:140px;transition:all .25s;background-size:100% 100%,300% 100%;animation:border-flow 5s linear infinite}
.sm-i:hover{border-color:transparent;animation:border-flow 1.5s linear infinite}
.sm-l{font-size:11px;color:var(--dim);letter-spacing:.06em;text-transform:uppercase;margin-bottom:4px}
.sm-v{font-size:26px;font-weight:700;letter-spacing:-.5px;line-height:1.2}
.sm-v .nu{font-size:12px;color:var(--dim);font-weight:400}
.ct{padding:24px 48px;max-width:1600px;margin:0 auto}
.sc{font-size:14px;color:var(--dim);margin-bottom:16px;display:flex;align-items:center;gap:10px;letter-spacing:.06em}
.sc::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,var(--brd),transparent)}
.sc .sc-badge{font-size:11px;padding:3px 10px;border-radius:10px;background:rgba(176,144,255,.15);color:var(--violet);font-weight:500}
.gr{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-bottom:26px}
.cd{background:linear-gradient(var(--s1),var(--s1)) padding-box,linear-gradient(90deg,var(--cyan),var(--violet),var(--cyan),var(--violet),var(--cyan)) border-box;border:2px solid transparent;border-radius:18px;padding:22px;position:relative;overflow:hidden;transition:all .35s ease;animation:border-flow 6s linear infinite;background-size:100% 100%,300% 100%}
.cd:hover{border-color:transparent;box-shadow:0 0 35px rgba(90,208,224,.1),0 0 60px rgba(123,93,191,.04);animation:border-flow 1.5s linear infinite}
.cd-glow{position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);opacity:.5;animation:shimmer 5s ease-in-out infinite}
.cd-hd{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}
.cd-n{font-size:17px;font-weight:600;letter-spacing:.04em}
.cd-sub{font-size:11px;color:var(--dim);margin-top:3px;font-family:var(--mono)}
.cd-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0;margin-top:6px}
.cd-dot.ok{background:var(--ok);box-shadow:0 0 16px var(--ok-glow);animation:breathe-dot 3s ease-in-out infinite}
.cd-dot.er{background:var(--err);box-shadow:0 0 16px rgba(255,74,110,.4);animation:breathe-dot 2s ease-in-out infinite}
.rs{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:8px}
.rb{position:relative}
.rb-h{display:flex;justify-content:space-between;font-size:11px;color:var(--dim);margin-bottom:4px}
.rb-b{height:5px;background:rgba(16,24,40,.5);border-radius:3px;overflow:hidden}
.rb-f{height:100%;border-radius:3px;transition:width 1.5s ease}
.rb-f.mem{background:linear-gradient(90deg,var(--cyan),#7de8f5)}
.rb-f.dsk{background:linear-gradient(90deg,var(--violet),#c4b5fd)}
.rb-f.cpu{background:linear-gradient(90deg,var(--cyan),var(--violet))}
.cd-info{display:flex;gap:8px;flex-wrap:wrap;font-size:11px;margin-top:6px}
.cd-info s{padding:3px 8px;border-radius:5px;background:rgba(16,24,40,.5);color:var(--dim)}
.cd-info s.g{color:var(--ok);border:1px solid rgba(64,184,122,.2)}
.ml{margin-top:10px;padding-top:10px;border-top:1px solid var(--brd)}
.ml-h{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;display:flex;justify-content:space-between}
.ml-g{display:flex;flex-wrap:wrap;gap:4px}
.ml-t{font-size:10px;padding:3px 8px;border-radius:4px;background:rgba(16,24,40,.5);color:var(--dim);border:1px solid transparent}
.ml-t.act{border-color:rgba(64,184,122,.25);color:var(--ok);background:rgba(64,184,122,.06)}
.ml-t.idl{border-color:rgba(106,128,160,.1);color:var(--dim)}
.pool-sxn{background:rgba(12,19,32,.6);border:1px solid rgba(176,144,255,.15);border-radius:18px;padding:24px;margin-bottom:26px}
.pool-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}
.pool-hd-l{display:flex;align-items:center;gap:12px}
.pool-hd-l .ico{width:10px;height:10px;border-radius:50%;background:var(--violet);box-shadow:0 0 14px var(--violet-glow);animation:breathe-dot 3s ease-in-out infinite}
.pool-hd-tt{font-size:16px;font-weight:600;color:var(--violet)}
.pool-hd-st{font-size:11px;color:var(--dim)}
.pool-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px}
.pool-meter{background:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:14px;padding:16px}
.pool-meter-l{font-size:11px;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between}
.pool-meter-bar{height:7px;background:rgba(16,24,40,.5);border-radius:4px;overflow:hidden;margin-bottom:6px}
.pool-meter-fill{height:100%;border-radius:4px;transition:width 1.5s ease}
.pool-meter-fill.cpu{background:linear-gradient(90deg,var(--violet),#c4b5fd)}
.pool-meter-fill.mem{background:linear-gradient(90deg,var(--cyan),#7de8f5)}
.pool-meter-v{font-size:22px;font-weight:700;font-family:var(--mono);letter-spacing:-.5px}
.pool-meter-v .u{font-size:12px;color:var(--dim);font-weight:400}
.pool-nodes{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.pool-node{background:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:14px;padding:16px;transition:all .25s}
.pool-node:hover{border-color:rgba(176,144,255,.3)}
.pool-node-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.pool-node-n{font-size:14px;font-weight:600}
.pool-node-s{font-size:10px;color:var(--dim);font-family:var(--mono)}
.pool-node-st{width:9px;height:9px;border-radius:50%}
.pool-node-st.on{background:var(--ok);box-shadow:0 0 10px var(--ok-glow)}
.pool-node-st.off{background:var(--err)}
.pool-node-stats{display:grid;grid-template-columns:1fr 1fr;gap:4px 14px;font-size:11px}
.pool-node-stats .r{display:flex;justify-content:space-between;padding:2px 0}
.pool-node-stats .r .k{color:var(--dim)}
.pool-node-stats .r .v{color:var(--txt);font-family:var(--mono);font-size:10px}
.pool-mini-bar{height:4px;background:rgba(16,24,40,.5);border-radius:2px;overflow:hidden;margin:2px 0 6px}
.pool-mini-bar .f{height:100%;border-radius:2px;transition:width 1.5s ease}
.pool-mini-bar .f.cpu{background:var(--violet)}
.pool-mini-bar .f.mem{background:var(--cyan)}
.bt{display:flex;gap:18px;margin-bottom:0}
.bt-l{flex:2;background:rgba(12,19,32,.6);border:1px solid var(--brd);border-radius:18px;padding:20px;min-height:180px;display:flex;flex-direction:column}
.bt-lh{font-size:14px;color:var(--dim);margin-bottom:12px;display:flex;justify-content:space-between;align-items:center}
.bt-lh s{font-weight:400;font-size:11px}
.bt-lg{flex:1;background:rgba(8,12,20,.5);border:1px solid var(--brd);border-radius:10px;padding:12px 14px;font-family:var(--mono);font-size:12px;line-height:1.8;color:var(--ok);overflow-y:auto;max-height:160px}
.bt-le{font-size:14px;color:var(--dim);text-align:center;padding:40px 0}
.bt-r{flex:1;background:rgba(12,19,32,.6);border:1px solid var(--brd);border-radius:18px;padding:24px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:180px;transition:all .3s}
.bt-r.has-request{border-color:rgba(90,208,224,.3);box-shadow:0 0 30px rgba(90,208,224,.1)}
.bt-ra{font-size:14px;color:var(--dim);margin-bottom:8px}
.bt-rt{font-size:15px;color:var(--cyan);padding:8px 16px;background:rgba(90,208,224,.08);border:1px solid rgba(90,208,224,.2);border-radius:8px;margin-bottom:12px;font-weight:500}
.bt-b{display:flex;gap:14px;margin-top:8px}
.bt-ok{padding:12px 36px;background:var(--ok);color:#000;border:none;border-radius:10px;font-size:16px;font-weight:600;cursor:pointer}
.bt-ok:hover{box-shadow:0 0 30px var(--ok-glow);transform:scale(1.03)}
.bt-no{padding:12px 36px;background:transparent;color:var(--dim);border:1px solid var(--brd);border-radius:10px;font-size:16px;cursor:pointer}
.bt-no:hover{border-color:var(--err);color:var(--err);box-shadow:0 0 20px rgba(255,74,110,.15)}
.rcpt{background:rgba(16,24,40,.4);border:1px solid rgba(90,208,224,.15);border-radius:10px;padding:12px 16px;margin-bottom:8px;display:none;flex-shrink:0}
.rcpt-st{font-size:11px;color:var(--dim);margin-bottom:5px;display:flex;gap:8px;align-items:center}
.rcpt-st .sg{padding:2px 8px;border-radius:10px;font-size:10px}
.rcpt-st .sg.w{color:var(--warn);border:1px solid var(--warn)}
.rcpt-st .sg.r{color:var(--ok);border:1px solid var(--ok)}
.rcpt-st .sg.x{color:var(--cyan);border:1px solid var(--cyan)}
.rcpt-st .sg.d{color:var(--ok)}
.rcpt-st .sg.f{color:var(--err);border:1px solid var(--err)}
.rcpt-msg{font-size:14px;line-height:1.5;color:var(--txt)}
.toast{position:fixed;bottom:28px;right:28px;background:rgba(12,19,32,.9);border:1px solid var(--ok);border-radius:10px;padding:12px 24px;font-size:13px;color:var(--ok);z-index:200;max-width:400px;box-shadow:0 8px 32px rgba(0,0,0,.4)}
.ft{background:rgba(8,12,20,.6);border-top:1px solid var(--brd);padding:14px 48px;display:flex;justify-content:space-between;font-size:11px;color:var(--dim)}
@media(max-width:1100px){.gr,.pool-nodes{grid-template-columns:1fr 1fr}.pool-summary{grid-template-columns:repeat(2,1fr)}}
@media(max-width:760px){.gr,.pool-nodes,.pool-summary{grid-template-columns:1fr}.bt{flex-direction:column}.hd,.sm,.ct,.ft{padding-left:20px;padding-right:20px}}
</style>
</head>
<body>
<header class="hd"><div class="hd-l"><div class="hd-logo"></div><div><div class="hd-tt">领地健康看板</div><div class="hd-st">六域主权 · 服务器实时监控</div></div></div><div class="hd-r"><div class="hd-tm" id="gl-clock">--:--:--</div><div class="hd-ss"><span id="gl-st" style="color:var(--ok)">全境正常</span><span style="color:var(--dim)">|</span><span id="gl-uptime" style="color:var(--dim)"></span></div></div></header>
<div class="sm"><div class="sm-i"><div class="sm-l">服务器</div><div class="sm-v" style="color:var(--ok)" id="sm-srv">--</div></div><div class="sm-i"><div class="sm-l">内存</div><div class="sm-v" style="color:var(--cyan)" id="sm-mem">--</div></div><div class="sm-i"><div class="sm-l">CPU</div><div class="sm-v" style="color:var(--violet)" id="sm-cpu">--</div></div><div class="sm-i"><div class="sm-l">报警</div><div class="sm-v" style="color:var(--ok)" id="sm-alert">0</div></div></div>
<main class="ct">
<div class="sc">服务器集群 · 六域主权</div><div class="gr" id="server-grid"></div>
<div class="sc">算力汇聚池 <span class="sc-badge">AGE v1.0</span></div>
<div class="pool-sxn"><div class="pool-hd"><div class="pool-hd-l"><div class="ico"></div><div class="pool-hd-tt">铸渊 · 虚拟算力池</div><div class="pool-hd-st" id="pool-updated">--</div></div><div style="font-size:11px;color:var(--dim)" id="pool-tasks">--</div></div><div class="pool-summary" id="pool-meter-grid"></div><div class="pool-nodes" id="pool-node-grid"></div></div>
<div class="bt">
<div class="bt-l"><div class="bt-lh"><span>铸渊操作流水</span><s>只读</s></div><div id="receipt-container"></div><div class="bt-lg" id="log-output"><div class="bt-le">等待铸渊发起操作...</div></div></div>
<div class="bt-r" style="display:flex;flex-direction:row;gap:12px">
<div class="bt-r" id="auth-panel" style="flex:1;min-height:auto;padding:20px"><div class="bt-ra">暂无待处理的操作请求</div><div style="font-size:12px;color:var(--dim);margin-top:6px">铸渊操作由主权者发起</div></div>
<div class="bt-r" style="flex:1;min-height:auto;padding:18px">
<div style="font-size:13px;color:var(--violet);margin-bottom:10px;font-weight:600">🔑 密钥投递</div>
<div style="display:flex;flex-direction:column;gap:8px;width:100%">
<input placeholder="API Key / Token / Password" id="key-value" style="padding:10px 14px;background:rgba(8,12,20,.5);border:1px solid var(--brd);border-radius:8px;color:var(--txt);font-size:12px;outline:none;font-family:var(--mono);width:100%">
<div style="display:flex;gap:8px"><input placeholder="标签(如:AutoDL)" id="key-label" style="flex:1;padding:10px 14px;background:rgba(8,12,20,.5);border:1px solid var(--brd);border-radius:8px;color:var(--txt);font-size:12px;outline:none"><button onclick="submitKey()" style="padding:10px 16px;background:linear-gradient(135deg,rgba(42,157,191,.4),rgba(123,93,191,.3));border:1px solid rgba(90,208,224,.2);border-radius:8px;color:var(--txt);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap">投递</button></div>
<div id="key-status" style="font-size:11px;color:var(--dim);min-height:16px"></div>
</div>
</div>
</div>
</div>
</main>
<footer class="ft"><span>曜冥纪元 · HoloLake Era | 六域归一 · AGE v1.0</span><span>国作登字-2026-A-00037559</span></footer>
<script>
const API=window.location.pathname.startsWith('/console')?'/console/api':'/api';let iv;
const MODULES={'BS-GZ-006':['forgejo代码仓库','mcp-server','console-server','portal前端','gatekeeper守护','nginx反代','系统日志','监控采集'],'BS-SG-001':['gatekeeper守护','模型推理','cos同步','配置服务','日志聚合','性能监控','安全审计'],'BS-SG-002':['gatekeeper守护','web服务','cdn加速','缓存层','负载均衡','故障转移'],'BS-SG-003':['gatekeeper守护','备份服务','存储节点','数据同步','归档服务','快照管理'],'ZY-SG-006':['gatekeeper守护','计算节点','推理服务','内存缓存','训练流水线'],'BS-SH-005':['gatekeeper守护','容灾节点','冷备存储','健康检查','数据校验']};
function $(s){return document.getElementById(s)}
function toast(m,c){const t=document.createElement('div');t.className='toast';t.style.borderColor=c||'var(--ok)';t.style.color=c||'var(--ok)';t.textContent=m;document.body.appendChild(t);setTimeout(function(){t.style.opacity='0';setTimeout(function(){t.remove()},300)},4000)}
async function fetchAPI(p){try{const r=await fetch(API+p);return await r.json()}catch(e){return null}}
function renderCards(srvData,poolData){const sv=(srvData&&srvData.servers)||[];const grid=$('server-grid');if(!sv.length){grid.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:50px;color:var(--dim);font-size:16px">正在采集服务器数据...</div>';return}const nodes=(poolData&&poolData.nodes)||{};const alive=sv.filter(function(s){return s.alive}).length;const glSt=$('gl-st');glSt.textContent=alive===sv.length?'全境正常':(alive>0?alive+'/'+sv.length+' 在线':'全境离线');glSt.style.color=alive===sv.length?'var(--ok)':(alive>0?'var(--warn)':'var(--err)');$('sm-srv').innerHTML=alive+'/6 <span class="nu">在线</span>';var totalMemPct=0,memCount=0,totalCpuLoad=0,cpuCount=0;grid.innerHTML=sv.map(function(s,i){var ok=s.alive?'ok':'er';var st=s.alive?'在线':'离线';var lt=s.latency?s.latency+'ms':'-';var node=nodes[s.code]||{};var spec=node.spec||{cpu:2,mem_gb:4};var memTotal=node.mem_total_mb||0;var memAvail=node.mem_avail_mb||0;var memPct=memTotal>0?Math.round((memTotal-memAvail)/memTotal*100):0;var diskPct=node.disk_used_pct||0;var cpuLoad=node.cpu_load||0;var cpuPct=spec.cpu>0?Math.min(Math.round(cpuLoad/spec.cpu*100),100):0;var uptime=node.uptime_hours||0;var tasks=node.tasks||0;if(s.alive){totalMemPct+=memPct;memCount++;totalCpuLoad+=cpuLoad;cpuCount++}var mods=MODULES[s.code]||['基础服务'];var gDel=((i%6)*0.5)+'s';return '<div class="cd"><div class="cd-glow" style="animation-delay:'+gDel+'"></div><div class="cd-hd"><div><div class="cd-n">'+s.name+'</div><div class="cd-sub">'+s.code+' · '+s.ip+'</div></div><div class="cd-dot '+ok+'"></div></div><div class="rs"><div class="rb"><div class="rb-h"><span>CPU</span><span>'+cpuPct+'%</span></div><div class="rb-b"><div class="rb-f cpu" style="width:'+cpuPct+'%"></div></div></div><div class="rb"><div class="rb-h"><span>内存</span><span>'+(s.alive?memPct:'--')+'%</span></div><div class="rb-b"><div class="rb-f mem" style="width:'+(s.alive?memPct:0)+'%"></div></div></div><div class="rb"><div class="rb-h"><span>磁盘</span><span>'+(s.alive?diskPct:'--')+'%</span></div><div class="rb-b"><div class="rb-f dsk" style="width:'+(s.alive?diskPct:0)+'%"></div></div></div></div><div class="cd-info"><s'+(s.alive?' class="g"':'')+'>'+lt+'</s><s'+(s.alive?' class="g"':'')+'>'+(uptime>0?uptime+'h':'--')+'</s><s>任务 '+tasks+'</s><s>'+st+'</s></div><div class="ml"><div class="ml-h"><span>模块</span></div><div class="ml-g">'+mods.map(function(m){var r=Math.random();return '<span class="ml-t '+(s.alive&&r>0.3?'act':'idl')+'">'+m+'</span>'}).join('')+'</div></div></div>'}).join('');$('sm-mem').innerHTML=(memCount>0?Math.round(totalMemPct/memCount):'--')+'% <span class="nu">avg</span>';$('sm-cpu').innerHTML=(cpuCount>0?(totalCpuLoad/cpuCount).toFixed(2):'--')+' <span class="nu">load</span>'}
function renderPool(poolData){if(!poolData||!poolData.pool){$('pool-meter-grid').innerHTML='<div style="grid-column:1/-1;text-align:center;padding:24px;color:var(--dim);font-size:14px">算力池数据采集中...</div>';$('pool-node-grid').innerHTML='';return}var p=poolData.pool;var nodes=poolData.nodes||{};var totalSpec=poolData.total_spec||{cpu:14,mem_gb:26};var activeTasks=poolData.active_tasks||0;var updated=poolData.updated_at?new Date(poolData.updated_at).toLocaleTimeString('zh-CN',{hour12:false}):'--';$('pool-updated').textContent='采集时间:'+updated;$('pool-tasks').textContent='活跃任务:'+activeTasks;var cpuPct=p.cpu_total>0?Math.round(p.cpu_used/p.cpu_total*100):0;var memPct=p.mem_total_gb>0?Math.round(p.mem_used_gb/p.mem_total_gb*100):0;$('pool-meter-grid').innerHTML='<div class="pool-meter"><div class="pool-meter-l"><span>CPU 使用率</span><span>'+cpuPct+'%</span></div><div class="pool-meter-bar"><div class="pool-meter-fill cpu" style="width:'+cpuPct+'%"></div></div><div class="pool-meter-v">'+(p.cpu_available||0).toFixed(1)+' <span class="u">/'+p.cpu_total+' 核可用</span></div></div><div class="pool-meter"><div class="pool-meter-l"><span>内存使用率</span><span>'+memPct+'%</span></div><div class="pool-meter-bar"><div class="pool-meter-fill mem" style="width:'+memPct+'%"></div></div><div class="pool-meter-v">'+(p.mem_available_gb||0).toFixed(1)+' <span class="u">/'+p.mem_total_gb+' GB 可用</span></div></div><div class="pool-meter"><div class="pool-meter-l"><span>节点</span><span style="color:var(--ok)">'+(p.online_nodes||0)+'/'+(p.total_nodes||6)+' 在线</span></div><div style="margin:8px 0;display:flex;gap:4px;flex-wrap:wrap">'+Object.keys(nodes).map(function(code){var n=nodes[code];return '<span style="width:16px;height:16px;border-radius:3px;background:'+(n&&n.online?'var(--ok)':'var(--err)')+';opacity:.7" title="'+code+'"></span>'}).join('')+'</div><div class="pool-meter-v" style="font-size:16px">'+(p.online_nodes||0)+' <span class="u">节点在线</span></div></div><div class="pool-meter"><div class="pool-meter-l"><span>规格总览</span></div><div style="margin:4px 0;font-size:12px;color:var(--dim);line-height:1.8">总 CPU'+totalSpec.cpu+' 核<br>总内存:'+totalSpec.mem_gb+' GB<br>已用 CPU'+(p.cpu_used||0).toFixed(1)+' 核<br>已用内存:'+(p.mem_used_gb||0).toFixed(1)+' GB</div></div>';var nodeEntries=Object.entries(nodes);if(!nodeEntries.length){$('pool-node-grid').innerHTML='<div style="grid-column:1/-1;text-align:center;padding:18px;color:var(--dim);font-size:12px">等待节点上报...</div>';return}$('pool-node-grid').innerHTML=nodeEntries.map(function(entry){var code=entry[0],n=entry[1];var spec=n.spec||{cpu:2,mem_gb:4};var memTotal=n.mem_total_mb||0;var memAvail=n.mem_avail_mb||0;var memPct=memTotal>0?Math.round((memTotal-memAvail)/memTotal*100):0;var diskPct=n.disk_used_pct||0;var cpuLoad=n.cpu_load||0;var cpuPct=spec.cpu>0?Math.min(Math.round(cpuLoad/spec.cpu*100),100):0;var uptime=n.uptime_hours||0;var tasks=n.tasks||0;return '<div class="pool-node"><div class="pool-node-hd"><div><div class="pool-node-n">'+(n.name||code)+'</div><div class="pool-node-s">'+code+' · '+spec.cpu+'核/'+spec.mem_gb+'G</div></div><div class="pool-node-st '+(n.online?'on':'off')+'"></div></div><div class="pool-mini-bar"><div class="f cpu" style="width:'+cpuPct+'%"></div></div><div class="pool-node-stats"><div class="r"><span class="k">CPU</span><span class="v">'+cpuLoad.toFixed(2)+' ('+cpuPct+'%)</span></div><div class="r"><span class="k">内存</span><span class="v">'+memPct+'%</span></div><div class="r"><span class="k">磁盘</span><span class="v">'+diskPct+'%</span></div><div class="r"><span class="k">运行</span><span class="v">'+(uptime>0?uptime+'h':'--')+'</span></div><div class="r"><span class="k">任务</span><span class="v">'+tasks+'</span></div><div class="r"><span class="k">延迟</span><span class="v">'+(n.last_seen?Math.floor((Date.now()-new Date(n.last_seen).getTime())/1000)+'s':'--')+'</span></div></div></div>'}).join('')}
function renderSessionsAndAuth(sInfo,pInfo){var p=(pInfo&&pInfo.requests)||[];var ss=(sInfo&&sInfo.sessions)||[];var ap=$('auth-panel');var rc=$('receipt-container');var lg=$('log-output');if(ss.length){rc.innerHTML=ss.map(function(s){var sc=({'敲门中':'w','已授权':'r','执行中':'x','完成':'d','已拒绝':'f'})[s.operation_stage]||'w';return '<div class="rcpt" style="display:block"><div class="rcpt-st"><span>'+s.server_name+'</span><span class="sg '+sc+'">'+s.operation_stage+'</span><span style="color:var(--dim)">'+s.age_seconds+'s</span></div><div class="rcpt-msg">'+s.human_readable+'</div></div>'}).join('')}else{rc.innerHTML=''}if(p.length){var r=p[0];var ago=Math.floor((Date.now()-new Date(r.created_at).getTime())/1000);ap.className='bt-r has-request';ap.innerHTML='<div class="bt-ra">铸渊请求对 <span style="color:var(--cyan);font-weight:600">'+r.target_name+'</span> 执行操作</div><div class="bt-rt">'+r.reason+'</div><div style="font-size:12px;color:var(--dim);margin-bottom:12px">'+ago+'秒前</div><div class="bt-b"><button class="bt-ok" onclick="authOp(\''+r.id+'\')">授权</button><button class="bt-no" onclick="rejOp(\''+r.id+'\')">拒绝</button></div>'}else{ap.className='bt-r';ap.innerHTML='<div class="bt-ra">暂无待处理的操作请求</div><div style="font-size:12px;color:var(--dim);margin-top:6px">铸渊操作由主权者发起</div>'}if(!p.length&&!ss.length&&!lg.querySelector('.bt-le'))lg.innerHTML='<div class="bt-le">等待铸渊发起操作...</div>'}
async function authOp(id){var r=await fetch(API+'/authorize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});var d=await r.json();if(d.ok){toast('已确认信任,铸渊将继续执行');addLog('授权','冰朔已确认');pollAll()}else{toast('授权异常','var(--err)')}}
async function rejOp(id){await fetch(API+'/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});toast('已拒绝','var(--err)');addLog('拒绝','冰朔未确认');pollAll()}
function addLog(tag,msg){var lg=$('log-output');var e=lg.querySelector('.bt-le');if(e)e.remove();var t=new Date().toLocaleTimeString('zh-CN',{hour12:false});var d=document.createElement('div');d.innerHTML='<span style="color:var(--dim)">'+t+'</span> <span style="color:'+(tag==='授权'?'var(--ok)':'var(--err)')+'">['+tag+']</span> '+msg;lg.appendChild(d);lg.scrollTop=lg.scrollHeight}
async function pollAll(){var results=await Promise.all([fetchAPI('/servers'),fetchAPI('/pending'),fetchAPI('/terminal/sessions'),fetchAPI('/pool/status')]);var srv=results[0],pending=results[1],sessions=results[2],pool=results[3];if(srv)renderCards(srv,pool&&pool.pool);renderSessionsAndAuth(sessions,pending);if(pool&&pool.pool)renderPool(pool.pool)}
function updateClock(){$('gl-clock').textContent=new Date().toLocaleTimeString('zh-CN',{hour12:false})}
(function(){updateClock();pollAll();loadKeys();iv=setInterval(pollAll,5000);setInterval(updateClock,1000)})();
async function submitKey(){var v=document.getElementById('key-value').value.trim();if(!v)return;var label=document.getElementById('key-label').value.trim()||'未命名';try{var r=await fetch(API+'/keys/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({value:v,label:label})});var d=await r.json();if(d.ok){document.getElementById('key-value').value='';document.getElementById('key-label').value='';document.getElementById('key-status').textContent='✓ 已投递 · ID:'+d.id;document.getElementById('key-status').style.color='var(--ok)';loadKeys()}else{document.getElementById('key-status').textContent=d.error;document.getElementById('key-status').style.color='var(--err)'}}catch(e){document.getElementById('key-status').textContent='连接失败';document.getElementById('key-status').style.color='var(--err)'}}
async function loadKeys(){try{var r=await fetch(API+'/keys');var d=await r.json();if(d.ok&&d.keys.length>0){var el=document.getElementById('key-status');el.textContent='已有 '+d.keys.length+' 个密钥可用';el.style.color='var(--cyan)'}}catch(e){}}
async function delKey(id){await fetch(API+'/keys/'+id,{method:'DELETE'});loadKeys()}
</script>
</body>
</html>