Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

703 lines
43 KiB
HTML
Raw Permalink 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);--cyan-bright:#8ef0ff;
--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-border{0%,100%{border-color:rgba(90,208,224,.08)}50%{border-color:rgba(90,208,224,.25)}}
@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%)}}
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:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:12px;padding:12px 22px;flex-shrink:0;min-width:140px;transition:all .25s;animation:breathe-border 5s ease-in-out infinite}
.sm-i:hover{border-color:var(--brd-hover)}
.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:rgba(12,19,32,.6);border:1px solid var(--brd);border-radius:18px;padding:22px;position:relative;overflow:hidden;transition:all .35s ease;animation:breathe-card 6s ease-in-out infinite}
.cd:hover{border-color:var(--brd-hover);box-shadow:0 0 35px rgba(90,208,224,.1),0 0 60px rgba(123,93,191,.04)}
.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.static{background:var(--ok);box-shadow:0 0 8px var(--ok-glow);animation:none}
.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%;flex-shrink:0}
.pool-node-st.on{background:var(--ok);box-shadow:0 0 10px var(--ok-glow)}
.pool-node-st.off{background:var(--err);box-shadow:0 0 10px rgba(255,74,110,.3)}
.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:26px}
.bt-l{flex:2;background:rgba(12,19,32,.6);border:1px solid var(--brd);border-radius:18px;padding:20px;display:flex;flex-direction:column;min-height:200px}
.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:11px;line-height:1.7;overflow-y:auto;max-height:180px;min-height:80px}
.bt-lg .log-line{padding:2px 0;border-bottom:1px solid rgba(255,255,255,.02);display:flex;gap:8px;align-items:baseline}
.bt-lg .log-time{color:var(--text-faint,var(--dim));font-size:10px;white-space:nowrap;min-width:48px}
.bt-lg .log-act{font-weight:600;min-width:80px;font-size:10px}
.bt-lg .log-act.auth{color:var(--ok)}
.bt-lg .log-act.reject{color:var(--err)}
.bt-lg .log-act.info{color:var(--cyan)}
.bt-lg .log-act.warn{color:var(--warn)}
.bt-lg .log-act.err{color:var(--err)}
.bt-lg .log-detail{color:var(--dim);font-size:10px;word-break:break-all}
.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;transition:all .25s}
.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;transition:all .25s}
.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;transition:opacity .3s;box-shadow:0 8px 32px rgba(0,0,0,.4)}
.gpu-sxn{background:rgba(12,19,32,.6);border:1px solid rgba(90,208,224,.15);border-radius:18px;padding:24px;margin-bottom:26px}
.gpu-sxn-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.gpu-sxn-hd-l{display:flex;align-items:center;gap:12px}
.gpu-sxn-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.gpu-sxn-dot.on{background:var(--ok);box-shadow:0 0 14px var(--ok-glow);animation:breathe-dot 3s ease-in-out infinite}
.gpu-sxn-dot.off{background:var(--err);box-shadow:0 0 14px rgba(255,74,110,.3)}
.gpu-sxn-tt{font-size:16px;font-weight:600;color:var(--cyan)}
.gpu-sxn-st{font-size:11px;color:var(--dim)}
.gpu-grid{display:grid;grid-template-columns:2fr 1fr;gap:18px;margin-bottom:18px}
.gpu-card{background:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:14px;padding:18px;transition:all .25s}
.gpu-card:hover{border-color:rgba(90,208,224,.3)}
.gpu-card-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.gpu-card-n{font-size:14px;font-weight:600;color:var(--txt)}
.gpu-card-v{font-size:12px;color:var(--dim);font-family:var(--mono)}
.gpu-stat-row{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}
.gpu-stat-item{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.02)}
.gpu-stat-l{font-size:11px;color:var(--dim)}
.gpu-stat-v{font-size:13px;font-family:var(--mono);font-weight:600}
.gpu-stat-bar{height:5px;background:rgba(16,24,40,.5);border-radius:3px;overflow:hidden;margin-top:3px;grid-column:1/-1}
.gpu-stat-bar-fill{height:100%;border-radius:3px;transition:width 1.5s ease}
.gpu-stat-bar-fill.util{background:linear-gradient(90deg,var(--ok),var(--cyan))}
.gpu-stat-bar-fill.util.high{background:linear-gradient(90deg,var(--warn),var(--err))}
.gpu-stat-bar-fill.mem{background:linear-gradient(90deg,var(--cyan),var(--violet))}
.gpu-stat-bar-fill.temp{background:linear-gradient(90deg,var(--ok),var(--warn),var(--err))}
.train-card{background:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:14px;padding:18px}
.train-card-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.train-status{padding:3px 10px;border-radius:8px;font-size:10px;font-weight:600;letter-spacing:.04em}
.train-status.idle{background:rgba(106,128,160,.1);color:var(--dim)}
.train-status.running{background:rgba(64,184,122,.15);color:var(--ok)}
.train-status.error{background:rgba(255,74,110,.15);color:var(--err)}
.train-status.done{background:rgba(90,208,224,.12);color:var(--cyan)}
.train-info{font-size:11px;color:var(--dim);line-height:1.8;margin-top:8px}
.train-info span{color:var(--txt);font-family:var(--mono);font-size:12px}
.train-progress-bar{height:6px;background:rgba(16,24,40,.5);border-radius:3px;overflow:hidden;margin:10px 0}
.train-progress-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--cyan),var(--violet));transition:width 1.5s ease}
.loss-mini-chart{display:flex;align-items:flex-end;gap:2px;height:48px;margin-top:8px}
.loss-bar{flex:1;min-width:4px;border-radius:1px 1px 0 0;background:var(--cyan);opacity:.6;transition:height .5s ease}
.diary-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.diary-card{background:rgba(16,24,40,.5);border:1px solid var(--brd);border-radius:10px;padding:14px;transition:all .2s}
.diary-card:hover{border-color:rgba(176,144,255,.25)}
.diary-type{font-size:9px;padding:2px 7px;border-radius:6px;display:inline-block;margin-bottom:6px;font-weight:600;letter-spacing:.04em}
.diary-type.decision{background:rgba(90,208,224,.15);color:var(--cyan)}
.diary-type.fix{background:rgba(64,184,122,.15);color:var(--ok)}
.diary-type.error{background:rgba(255,74,110,.15);color:var(--err)}
.diary-type.checkpoint{background:rgba(176,144,255,.15);color:var(--violet)}
.diary-type.info{background:rgba(106,128,160,.1);color:var(--dim)}
.diary-title{font-size:12px;font-weight:600;color:var(--txt);margin-bottom:4px;letter-spacing:.03em}
.diary-desc{font-size:10px;color:var(--dim);line-height:1.5}
.diary-time{font-size:9px;color:var(--text-faint,var(--dim));margin-top:6px}
.agent-offline{text-align:center;padding:32px 0;color:var(--dim);font-size:13px;letter-spacing:.04em}
@media(max-width:1100px){.gpu-grid,.diary-grid{grid-template-columns:1fr}}
@media(max-width:760px){.gpu-grid{grid-template-columns:1fr}.diary-grid{grid-template-columns:1fr 1fr}}
.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">六域主权 · 11台服务器实时监控</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" id="sc-servers">服务器集群 · 全境监控</div>
<div class="gr" id="server-grid"></div>
<div class="sc">算力汇聚池 <span class="sc-badge">AGE v1.0</span></div>
<div class="pool-sxn" id="pool-section">
<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="sc">3090 GPU · 铸渊Agent训练台 <span class="sc-badge" id="agent-status-badge" style="background:rgba(106,128,160,.1);color:var(--dim)">未连接</span></div>
<div class="gpu-sxn" id="gpu-section">
<div class="gpu-sxn-hd">
<div class="gpu-sxn-hd-l">
<div class="gpu-sxn-dot off" id="agent-dot"></div>
<div class="gpu-sxn-tt">铸渊Agent · 3090 GPU</div>
<div class="gpu-sxn-st" id="agent-updated">等待连接...</div>
</div>
<div style="display:flex;align-items:center;gap:14px">
<span style="font-size:11px;color:var(--dim)" id="agent-hostname">--</span>
</div>
</div>
<div class="gpu-grid">
<div class="gpu-card" id="gpu-info-card">
<div class="agent-offline">等待铸渊Agent上报GPU数据...</div>
</div>
<div class="train-card" id="train-card">
<div class="train-card-hd">
<span style="font-size:13px;font-weight:600;color:var(--txt)">训练进度</span>
<span class="train-status idle" id="train-status-badge">idle</span>
</div>
<div class="agent-offline" id="train-offline">等待训练任务...</div>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:11px;color:var(--dim);letter-spacing:.04em">铸渊Agent日记</span>
<span style="font-size:10px;color:var(--dim)" id="diary-count"></span>
</div>
<div class="diary-grid" id="diary-grid">
<div class="agent-offline" style="grid-column:1/-1">Agent尚未产生日记条目</div>
</div>
</div>
<div class="bt">
<div class="bt-l">
<div class="bt-lh"><span>铸渊操作流水 · 滚动日志</span><s id="log-count">0条</s></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:18px">
<div class="bt-r" id="auth-panel" style="flex:1;min-height:auto">
<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:20px">
<div style="font-size:13px;color:var(--violet);margin-bottom:12px;font-weight:600">🔑 铸渊密钥投递</div>
<div style="display:flex;flex-direction:column;gap:8px;width:100%">
<input placeholder="密钥 / API Key / Token" id="key-value" style="padding:12px 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:12px 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:12px 20px;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:13px;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';
var iv, serverData = [];
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守护','容灾节点','冷备存储','健康检查','数据校验'],
'AW-GZ-001':['企业代码仓库','CI/CD流水线','团队开发','监控采集','备份服务','gatekeeper守护'],
'AW-SH-002':['灾备节点','数据同步','快照管理','故障转移','gatekeeper守护'],
'AW-GZ-003':['灾备节点','冷备存储','健康检查','数据校验','gatekeeper守护']
};
function $(s) { return document.getElementById(s) }
function toast(m, c) {
var 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 { var r = await fetch(API + p); return await r.json() }
catch (e) { return null }
}
function renderCards(srvData) {
var sv = (srvData && srvData.servers) || [];
if (!sv.length) {
$('server-grid').innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:50px;color:var(--dim);font-size:16px">正在采集服务器数据...</div>';
return;
}
var alive = sv.filter(function(s) { return s.alive }).length;
var activeP = sv.filter(function(s) { return s.persona_active }).length;
var total = sv.length;
$('gl-st').textContent = activeP > 0 ? activeP + '人格体在线' : (alive === total ? '全境正常' : (alive > 0 ? alive + '/' + total + ' 在线' : '全境离线'));
$('gl-st').style.color = activeP > 0 ? 'var(--cyan)' : (alive === total ? 'var(--ok)' : (alive > 0 ? 'var(--warn)' : 'var(--err)'));
$('sm-srv').innerHTML = activeP+'<span class="nu">活跃</span> / '+alive+'/'+total+' <span class="nu">在线</span>';
$('sc-servers').innerHTML = '服务器集群 · ' + total + '台主机 <span class="sc-badge">冰朔6 + 企业3</span>';
var totalMemPct = 0, memCount = 0, totalCpuLoad = 0, cpuCount = 0;
$('server-grid').innerHTML = sv.map(function(s, i) {
var sAlive = s.alive;
var sActive = s.persona_active;
var dotClass = sActive ? 'ok' : (sAlive ? 'static' : 'er'); // 活跃=呼吸绿, 在线空闲=静态绿, 离线=红
var st = sActive ? '· ' + (s.persona_name || s.human_name || '活跃') : (sAlive ? '在线(空闲)' : '离线');
var lt = s.latency ? s.latency + 'ms' : '-';
var spec = s.spec || { cpu: 2, mem_gb: 4, disk_gb: 50 };
var hw = s.hardware || {};
var cpuPct = hw.cpu_pct !== undefined ? hw.cpu_pct : 0;
var memPct = hw.mem_pct !== undefined ? hw.mem_pct : 0;
var diskPct = hw.disk_used_pct !== undefined ? hw.disk_used_pct : 0;
var uptime = hw.uptime_h || 0;
var load1 = hw.load_1min || 0;
var memUsed = hw.mem_used_mb ? (hw.mem_used_mb / 1024).toFixed(1) : '--';
var memTotal = hw.mem_total_mb ? (hw.mem_total_mb / 1024).toFixed(0) : spec.mem_gb;
var diskUsed = hw.disk_used || '--';
var diskTotal = hw.disk_total || spec.disk_gb + 'G';
var source = hw.source || '--';
if (sAlive && hw.source === 'live') { totalMemPct += memPct; memCount++; totalCpuLoad += load1; 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 + ' · ' + spec.cpu + '核/' + spec.mem_gb + 'G/' + spec.disk_gb + 'G</div></div>' +
'<div class="cd-dot ' + dotClass + '"></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>内存 ' + memUsed + '/' + memTotal + 'G</span><span>' + memPct + '%</span></div><div class="rb-b"><div class="rb-f mem" style="width:' + memPct + '%"></div></div></div>' +
'<div class="rb"><div class="rb-h"><span>磁盘 ' + diskUsed + '/' + diskTotal + '</span><span>' + diskPct + '%</span></div><div class="rb-b"><div class="rb-f dsk" style="width:' + diskPct + '%"></div></div></div>' +
'</div>' +
'<div class="cd-info">' +
'<s' + (sAlive ? ' class="g"' : '') + '>' + lt + '</s>' +
'<s' + (sAlive ? ' class="g"' : '') + '>' + (uptime > 0 ? uptime + 'h' : '--') + '</s>' +
'<s>load ' + load1.toFixed(1) + '</s><s' + (sActive ? ' class="g"' : '') + '>' + st + '</s>' +
'</div>' +
'<div class="ml"><div class="ml-h"><span>模块</span><span style="font-size:9px;color:var(--text-faint)">数据源:' + source + '</span></div>' +
'<div class="ml-g">' + mods.map(function(m) {
return '<span class="ml-t ' + (sActive ? 'act' : (sAlive ? '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: 32, mem_gb: 77 };
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 || 9) + ' 在线</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">等待节点上报需要部署心跳Agent...</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 renderLogs(logData) {
var logs = (logData && logData.logs) || [];
var lg = $('log-output');
$('log-count').textContent = (logData && logData.total) ? logData.total + '条' : '0条';
if (!logs.length) {
lg.innerHTML = '<div class="bt-le">等待铸渊发起操作...</div>';
return;
}
lg.innerHTML = logs.map(function(l) {
var t = l.time ? new Date(l.time).toLocaleTimeString('zh-CN', { hour12: false }) : '';
var cls = l.level === 'error' ? 'err' : (l.level === 'warn' ? 'warn' : 'info');
return '<div class="log-line">' +
'<span class="log-time">' + t + '</span>' +
'<span class="log-act ' + cls + '">' + (l.action || '') + '</span>' +
'<span class="log-detail">' + (l.name || l.code || '') + ' ' + (l.detail || '') + '</span>' +
'</div>';
}).join('');
lg.scrollTop = 0;
}
function renderSessionsAndAuth(pInfo) {
var p = (pInfo && pInfo.requests) || [];
var ap = $('auth-panel');
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>';
}
}
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('已确认信任,铸渊将继续执行'); 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)');
pollAll();
}
async function pollAll() {
var results = await Promise.all([
fetchAPI('/servers'),
fetchAPI('/pending'),
fetchAPI('/pool/status'),
fetchAPI('/logs?limit=30'),
fetchAPI('/summary'),
fetchAPI('/agent/combined')
]);
var srv = results[0], pending = results[1], pool = results[2], logs = results[3], summary = results[4], agent = results[5];
if (srv) renderCards(srv);
renderSessionsAndAuth(pending);
if (pool && pool.pool) renderPool(pool.pool);
if (logs) renderLogs(logs);
if (agent) renderAgentData(agent);
if (summary && summary.alerts !== undefined) {
var c = summary.alerts;
$('sm-alert').innerHTML = c;
$('sm-alert').style.color = c > 0 ? 'var(--err)' : 'var(--ok)';
}
}
function updateClock() {
$('gl-clock').textContent = new Date().toLocaleTimeString('zh-CN', { hour12: false });
}
function renderAgentData(data) {
var dot = $('agent-dot');
var badge = $('agent-status-badge');
var updated = $('agent-updated');
var host = $('agent-hostname');
if (data && data.online && data.last_seen) {
dot.className = 'gpu-sxn-dot on';
badge.textContent = '在线';
badge.style.background = 'rgba(64,184,122,.15)';
badge.style.color = 'var(--ok)';
var age = Math.floor((Date.now() - new Date(data.last_seen).getTime()) / 1000);
updated.textContent = '上次上报:' + age + 's前';
host.textContent = (data.gpu && data.gpu.hostname) || '3090';
} else {
dot.className = 'gpu-sxn-dot off';
badge.textContent = '未连接';
badge.style.background = 'rgba(106,128,160,.1)';
badge.style.color = 'var(--dim)';
updated.textContent = '等待连接...';
host.textContent = '--';
}
if (data && data.gpu && data.gpu.gpus && data.gpu.gpus.length > 0) {
renderGPU(data.gpu.gpus);
} else if (!data || !data.online) {
$('gpu-info-card').innerHTML = '<div class="agent-offline">等待铸渊Agent上报GPU数据...</div>';
}
if (data && data.training) renderTraining(data.training);
if (data && data.diary && data.diary.length > 0) {
renderDiary(data.diary);
} else {
$('diary-grid').innerHTML = '<div class="agent-offline" style="grid-column:1/-1">Agent尚未产生日记条目</div>';
$('diary-count').textContent = '';
}
}
function renderGPU(gpus) {
var card = $('gpu-info-card');
card.innerHTML = gpus.map(function(g, i) {
var util = g.utilization_gpu || g.util || 0;
var memUsed = g.memory_used_mb || g.mem_used || 0;
var memTotal = g.memory_total_mb || g.mem_total || 24576;
var memPct = memTotal > 0 ? Math.round(memUsed / memTotal * 100) : 0;
var temp = g.temperature_gpu || g.temp || 0;
var power = g.power_draw_w || g.power || 0;
var fan = g.fan_speed || 0;
var utilClass = util > 80 ? 'util high' : 'util';
var tempGrad = temp > 80 ? 'temp' : (temp > 60 ? 'mem' : 'util');
return '<div class="gpu-card-hd"><div class="gpu-card-n">GPU ' + i + ' · ' + (g.name || 'NVIDIA RTX 3090') + '</div><div class="gpu-card-v">' + (g.gpu_serial || g.uuid || '') + '</div></div>' +
'<div class="gpu-stat-row">' +
'<div class="gpu-stat-item"><span class="gpu-stat-l">利用率</span><span class="gpu-stat-v" style="color:var(--cyan)">' + util + '%</span></div>' +
'<div class="gpu-stat-item"><span class="gpu-stat-l">显存</span><span class="gpu-stat-v" style="color:var(--violet)">' + (memUsed / 1024).toFixed(1) + ' / ' + (memTotal / 1024).toFixed(0) + ' GB</span></div>' +
'<div class="gpu-stat-bar"><div class="gpu-stat-bar-fill ' + utilClass + '" style="width:' + util + '%"></div></div>' +
'<div class="gpu-stat-bar"><div class="gpu-stat-bar-fill mem" style="width:' + memPct + '%"></div></div>' +
'<div class="gpu-stat-item"><span class="gpu-stat-l">温度</span><span class="gpu-stat-v" style="color:' + (temp > 75 ? 'var(--err)' : 'var(--txt)') + '">' + temp + '°C</span></div>' +
'<div class="gpu-stat-item"><span class="gpu-stat-l">功耗</span><span class="gpu-stat-v">' + power.toFixed(1) + 'W</span></div>' +
'<div class="gpu-stat-bar"><div class="gpu-stat-bar-fill ' + tempGrad + '" style="width:' + Math.min(temp / 90 * 100, 100) + '%"></div></div>' +
(fan > 0 ? '<div class="gpu-stat-item"><span class="gpu-stat-l">风扇</span><span class="gpu-stat-v">' + fan + '%</span></div>' : '') +
'</div>';
}).join('');
}
function renderTraining(t) {
var card = $('train-card');
var badge = $('train-status-badge');
var pct = t.total_steps > 0 ? Math.round(t.step / t.total_steps * 100) : 0;
var eta = t.eta_seconds ? formatDuration(t.eta_seconds) : '--';
var elapsed = t.elapsed_seconds ? formatDuration(t.elapsed_seconds) : '--';
var statusLabel = {idle:'空闲',running:'训练中',paused:'已暂停',error:'错误',done:'已完成'}[t.status] || t.status;
badge.className = 'train-status ' + t.status;
badge.textContent = statusLabel;
var lossBars = '';
if (t.loss_history && t.loss_history.length) {
var losses = t.loss_history.slice(-28);
var maxLoss = Math.max.apply(null, losses);
lossBars = '<div class="loss-mini-chart">' + losses.map(function(l) {
var h = maxLoss > 0 ? Math.max(l / maxLoss * 100, 2) : 2;
return '<div class="loss-bar" style="height:' + h + '%;opacity:' + (0.3 + l / maxLoss * 0.7) + '"></div>';
}).join('') + '</div>';
}
card.innerHTML =
'<div class="train-card-hd"><span style="font-size:13px;font-weight:600;color:var(--txt)">训练进度</span><span class="train-status ' + t.status + '" id="train-status-badge">' + statusLabel + '</span></div>' +
'<div style="font-size:13px;font-weight:600;color:var(--txt);margin-bottom:4px">' + (t.model_name || 'HLDP 3B微调') + '</div>' +
'<div class="train-info">' +
'<div>Step: <span>' + t.step + '</span> / <span>' + (t.total_steps || '?') + '</span> (' + pct + '%)</div>' +
'<div class="train-progress-bar"><div class="train-progress-fill" style="width:' + pct + '%"></div></div>' +
(t.loss !== null ? '<div>Loss: <span style="color:var(--cyan)">' + t.loss.toFixed(4) + '</span></div>' : '') +
'<div>ETA: <span>' + eta + '</span> | 已用: <span>' + elapsed + '</span></div>' +
(t.learning_rate ? '<div>LR: <span>' + t.learning_rate.toExponential(2) + '</span></div>' : '') +
'</div>' +
lossBars +
(t.message ? '<div style="font-size:10px;color:var(--dim);margin-top:6px;line-height:1.4">' + t.message + '</div>' : '');
}
function renderDiary(entries) {
var types = {decision:'决策',fix:'修复',error:'错误',checkpoint:'检查点',info:'信息'};
$('diary-grid').innerHTML = entries.map(function(e) {
var t = e.timestamp ? new Date(e.timestamp).toLocaleTimeString('zh-CN',{hour12:false}) : '';
return '<div class="diary-card">' +
'<span class="diary-type ' + (e.type || 'info') + '">' + (types[e.type] || e.type) + '</span>' +
'<div class="diary-title">' + (e.title || '') + '</div>' +
(e.description ? '<div class="diary-desc">' + e.description.slice(0,80) + '</div>' : '') +
'<div class="diary-time">' + t + '</div>' +
'</div>';
}).join('');
$('diary-count').textContent = entries.length + ' 条';
}
function formatDuration(sec) {
var h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = Math.floor(sec % 60);
return (h > 0 ? h + 'h' : '') + (m > 0 ? m + 'm' : '') + s + 's';
}
(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){}}
</script>
</body>
</html>