684 lines
41 KiB
HTML
684 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>曜冥纪元 · 领地健康看板</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> |