684 lines
41 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);--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 breathe-live{0%,100%{opacity:.4}50%{opacity:1}}
@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.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: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;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)}
/* ── 3090 GPU Agent训练台 ── */
.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">六域主权 · 服务器实时监控</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" 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>只读</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: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';
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'), fetchAPI('/agent/combined')]);
var srv = results[0], pending = results[1], sessions = results[2], pool = results[3], agent = results[4];
if (srv) renderCards(srv, pool && pool.pool);
renderSessionsAndAuth(sessions, pending);
if (pool && pool.pool) renderPool(pool.pool);
if (agent) renderAgentData(agent);
}
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.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.gpu && data.gpu.gpus && data.gpu.gpus.length > 0) {
renderGPU(data.gpu.gpus);
} else if (!data.online) {
$('gpu-info-card').innerHTML = '<div class="agent-offline">等待铸渊Agent上报GPU数据...</div>';
}
if (data.training) {
renderTraining(data.training);
}
if (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){}}
async function delKey(id){await fetch(API+'/keys/'+id,{method:'DELETE'});loadKeys()}
</script>
</body>
</html>