feat: v3.0 full territory health dashboard with receipts
This commit is contained in:
parent
49cda91740
commit
f4158349fc
@ -3,207 +3,296 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>光湖服务器主控台</title>
|
||||
<title>光湖 · 领地健康看板</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--border:#1e3251;--accent:#4f8ef7;--text:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--r:14px;--rs:8px;--log-bg:#0a0e1a;--log-text:#a8d8a8;--log-green:#5faf5f}
|
||||
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;font-size:14px;padding:24px;max-width:1200px;margin:0 auto}
|
||||
h1{font-size:22px;margin-bottom:4px;display:flex;align-items:center;gap:10px}
|
||||
.sub{color:var(--dim);font-size:13px;margin-bottom:24px}
|
||||
.dashboard{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:20px}
|
||||
.card{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:16px;transition:border-color .3s}
|
||||
.card.alive{border-color:rgba(52,211,153,.3)}
|
||||
.card.dead{border-color:rgba(248,113,113,.3)}
|
||||
.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
||||
.card-name{font-size:14px;font-weight:500}
|
||||
.dot{width:10px;height:10px;border-radius:50%;display:inline-block}
|
||||
.dot.alive{background:var(--ok);box-shadow:0 0 8px rgba(52,211,153,.5)}
|
||||
.dot.dead{background:var(--err);box-shadow:0 0 8px rgba(248,113,113,.5)}
|
||||
.card-meta{display:flex;gap:12px;font-size:12px;color:var(--dim);margin-bottom:8px;flex-wrap:wrap}
|
||||
.card-meta span{background:var(--s2);padding:2px 8px;border-radius:4px}
|
||||
.card-status{font-size:12px}
|
||||
.card-status .ok{color:var(--ok)}
|
||||
.card-status .err{color:var(--err)}
|
||||
.last-seen{font-size:11px;color:var(--dim);margin-top:8px}
|
||||
.panel{background:var(--s1);border:1px solid var(--border);border-radius:var(--r);padding:20px;margin-bottom:20px}
|
||||
.panel h2{font-size:16px;margin-bottom:12px;color:var(--accent)}
|
||||
.request-box{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:12px 16px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center}
|
||||
.request-box .info{flex:1}
|
||||
.request-box .target{font-size:14px;font-weight:500}
|
||||
.request-box .reason{font-size:12px;color:var(--dim)}
|
||||
.btn{padding:8px 16px;border:none;border-radius:var(--rs);font-size:13px;cursor:pointer;font-family:inherit}
|
||||
.btn-ok{background:var(--ok);color:#000}
|
||||
.btn-ok:hover{opacity:.85}
|
||||
.btn-no{background:var(--s3);color:var(--text);border:1px solid var(--border);margin-left:8px}
|
||||
.btn-no:hover{border-color:var(--err)}
|
||||
.empty{color:var(--dim);font-size:13px;text-align:center;padding:20px}
|
||||
.refresh-btn{background:var(--s3);border:1px solid var(--border);color:var(--text);padding:6px 14px;border-radius:var(--rs);cursor:pointer;font-size:12px;font-family:inherit}
|
||||
.refresh-btn:hover{border-color:var(--accent)}
|
||||
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
||||
.header-right{display:flex;gap:10px;align-items:center}
|
||||
.toast{position:fixed;bottom:24px;right:24px;background:var(--s3);border:1px solid var(--ok);border-radius:var(--rs);padding:12px 20px;font-size:13px;color:var(--ok);display:none;z-index:100;max-width:400px}
|
||||
.breathing{animation:breathe 2s ease-in-out infinite}
|
||||
@keyframes breathe{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
:root{--bg:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--brd:#1e3251;--blue:#4f8ef7;--txt:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--r:14px;--rs:8px}
|
||||
body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;font-size:14px;min-height:100vh;margin:0}
|
||||
|
||||
/* Operation Log */
|
||||
.log-panel{background:var(--log-bg);border:1px solid var(--border);border-radius:var(--rs);min-height:180px;max-height:420px;overflow-y:auto;font-family:'SF Mono','Cascadia Code','Fira Code','Menlo',monospace;font-size:13px;line-height:1.7;color:var(--log-text);padding:12px 16px;margin-bottom:8px;white-space:pre-wrap;word-break:break-all}
|
||||
.log-panel .entry{display:flex;gap:8px;align-items:flex-start;padding:2px 0}
|
||||
.log-panel .time{color:var(--dim);font-size:11px;flex-shrink:0;min-width:60px}
|
||||
.log-panel .tag{font-size:11px;padding:1px 6px;border-radius:3px;flex-shrink:0;min-width:36px;text-align:center}
|
||||
.log-panel .tag.knock{color:var(--warn);border:1px solid var(--warn)}
|
||||
.log-panel .tag.auth{color:var(--ok);border:1px solid var(--ok)}
|
||||
.log-panel .tag.exec{color:var(--accent);border:1px solid var(--accent)}
|
||||
.log-panel .tag.done{color:var(--ok)}
|
||||
.log-panel .tag.fail{color:var(--err);border:1px solid var(--err)}
|
||||
.log-panel .tag.info{color:var(--dim);border:1px solid var(--dim)}
|
||||
.log-panel .msg{flex:1;font-size:13px}
|
||||
.log-panel .msg .cmd{color:var(--log-green)}
|
||||
.log-panel .msg .output{color:var(--log-text);display:block;padding-left:12px;white-space:pre-wrap;border-left:1px solid rgba(52,211,153,.2);margin-top:2px}
|
||||
.log-panel .msg .err{color:var(--err)}
|
||||
.log-empty{color:var(--dim);font-size:13px;text-align:center;padding:40px 20px}
|
||||
/* Header */
|
||||
.hd{background:var(--s1);border-bottom:1px solid var(--brd);padding:18px 28px;display:flex;justify-content:space-between;align-items:center}
|
||||
.hd-l{font-size:19px;font-weight:500;letter-spacing:.5px}
|
||||
.hd-s{font-size:12px;color:var(--dim);margin-top:3px}
|
||||
.hd-r{text-align:right}
|
||||
.hd-clock{font-size:12px;color:var(--blue);font-family:'SF Mono',monospace}
|
||||
.hd-st{font-size:11px;color:var(--ok);margin-top:1px}
|
||||
|
||||
/* Active Session Cards */
|
||||
.session-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:12px}
|
||||
.session-card{background:var(--s2);border:1px solid var(--border);border-radius:var(--rs);padding:10px 14px}
|
||||
.session-card .srv{font-size:13px;font-weight:500}
|
||||
.session-card .meta{font-size:11px;color:var(--dim);margin-top:4px;display:flex;gap:12px}
|
||||
.session-card .dot{width:8px;height:8px;display:inline-block;margin-right:4px}
|
||||
/* Summary */
|
||||
.sm{display:flex;gap:10px;padding:14px 28px;background:var(--s1);border-bottom:1px solid var(--brd)}
|
||||
.sm-i{background:var(--s2);border:1px solid var(--brd);border-radius:var(--rs);padding:7px 14px;flex:1}
|
||||
.sm-l{font-size:11px;color:var(--dim)}
|
||||
.sm-v{font-size:17px;font-weight:500;margin-top:1px}
|
||||
|
||||
/* Container */
|
||||
.ct{padding:20px 28px;max-width:1200px;margin:0 auto}
|
||||
|
||||
/* Grid */
|
||||
.gr{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:18px}
|
||||
|
||||
/* Card */
|
||||
.cd{background:var(--s1);border:1px solid rgba(52,211,153,.25);border-radius:12px;padding:16px;position:relative;overflow:hidden;cursor:default;transition:border-color .4s,box-shadow .4s}
|
||||
.cd:hover{border-color:rgba(79,142,247,.45)!important;box-shadow:0 0 20px rgba(79,142,247,.07)}
|
||||
.cd-g{position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--ok),transparent);opacity:.5}
|
||||
.cd-ht{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}
|
||||
.cd-n{font-size:13px;font-weight:500}
|
||||
.cd-ip{font-size:10px;color:var(--dim);margin-top:1px}
|
||||
.cd-dot{width:11px;height:11px;border-radius:50%;flex-shrink:0;margin-top:3px}
|
||||
.cd-dot.ok{background:var(--ok);box-shadow:0 0 10px rgba(52,211,153,.5);animation:pulse 2s ease-in-out infinite}
|
||||
.cd-dot.er{background:var(--err);box-shadow:0 0 10px rgba(248,113,113,.5)}
|
||||
|
||||
/* Resource bars */
|
||||
.rb{margin-bottom:6px}
|
||||
.rb-h{display:flex;justify-content:space-between;font-size:10px;color:var(--dim);margin-bottom:2px}
|
||||
.rb-b{height:3px;background:var(--s3);border-radius:2px;overflow:hidden}
|
||||
.rb-f{height:100%;border-radius:2px;transition:width 1.2s ease}
|
||||
.rb-f.mem{background:var(--blue)}
|
||||
.rb-f.dsk{background:#639922}
|
||||
|
||||
/* Tags */
|
||||
.tags{display:flex;gap:6px;font-size:10px;color:var(--dim);flex-wrap:wrap;margin-top:8px}
|
||||
.tags s{background:var(--s2);padding:1px 7px;border-radius:4px}
|
||||
|
||||
/* Bottom row */
|
||||
.bt{display:flex;gap:14px;margin-bottom:24px}
|
||||
.bt-l{flex:2;background:var(--s1);border:1px solid var(--brd);border-radius:12px;padding:14px}
|
||||
.bt-lh{font-size:12px;font-weight:500;color:var(--dim);margin-bottom:8px;display:flex;justify-content:space-between}
|
||||
.bt-log{background:#0a0e1a;border:1px solid var(--brd);border-radius:6px;padding:10px 12px;font-family:'SF Mono',monospace;font-size:11px;line-height:1.7;color:#a8d8a8;max-height:130px;overflow-y:auto}
|
||||
.bt-le{font-size:11px;color:var(--dim);text-align:center;padding:30px}
|
||||
|
||||
/* Auth card */
|
||||
.bt-r{flex:1;background:var(--s1);border:1px solid var(--brd);border-radius:12px;padding:14px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:160px}
|
||||
.bt-ra{font-size:12px;color:var(--dim);margin-bottom:6px}
|
||||
.bt-rt{font-size:11px;color:var(--warn);padding:3px 10px;background:var(--s3);border-radius:5px;margin-bottom:10px}
|
||||
.bt-btns{display:flex;gap:10px}
|
||||
.bt-ok{padding:9px 26px;background:var(--ok);color:#000;border:none;border-radius:7px;font-size:13px;font-weight:500;cursor:pointer;transition:all .25s}
|
||||
.bt-ok:hover{box-shadow:0 0 22px rgba(52,211,153,.5)}
|
||||
.bt-no{padding:9px 26px;background:var(--s3);color:var(--dim);border:1px solid var(--brd);border-radius:7px;font-size:13px;cursor:pointer;transition:all .25s}
|
||||
.bt-no:hover{border-color:var(--err);color:var(--err)}
|
||||
|
||||
/* Operation receipt (human readable) */
|
||||
.rcpt{background:var(--s2);border:1px solid rgba(79,142,247,.2);border-radius:8px;padding:10px 14px;margin-bottom:10px;display:none}
|
||||
.rcpt-st{font-size:10px;color:var(--dim);margin-bottom:4px;display:flex;gap:8px;align-items:center}
|
||||
.rcpt-st .stg{padding:1px 8px;border-radius:10px;font-size:10px}
|
||||
.rcpt-st .stg.w{color:var(--warn);border:1px solid var(--warn)}
|
||||
.rcpt-st .stg.r{color:var(--ok);border:1px solid var(--ok)}
|
||||
.rcpt-st .stg.x{color:var(--blue);border:1px solid var(--blue)}
|
||||
.rcpt-st .stg.d{color:var(--ok)}
|
||||
.rcpt-st .stg.f{color:var(--err);border:1px solid var(--err)}
|
||||
.rcpt-msg{font-size:13px;line-height:1.5;color:var(--txt)}
|
||||
|
||||
/* Footer */
|
||||
.ft{background:#0a0e1a;border-top:1px solid var(--brd);padding:10px 28px;display:flex;justify-content:space-between;font-size:10px;color:#4a5568}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.65;transform:scale(1.2)}}
|
||||
@keyframes glide{0%{transform:translateX(-100%)}50%{transform:translateX(100%)}100%{transform:translateX(200%)}}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width:800px){
|
||||
.gr{grid-template-columns:repeat(2,1fr)}
|
||||
.sm{flex-wrap:wrap}
|
||||
.bt{flex-direction:column}
|
||||
}
|
||||
@media(max-width:500px){
|
||||
.gr{grid-template-columns:1fr}
|
||||
.ct{padding:14px}
|
||||
.hd-l{font-size:16px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-bar">
|
||||
<div class="hd">
|
||||
<div>
|
||||
<h1><span>光湖服务器主控台</span></h1>
|
||||
<div class="sub">铸渊看门人 · 六台服务器实时状态</div>
|
||||
<div class="hd-l">光湖 · 领地健康看板</div>
|
||||
<div class="hd-s">主权者:冰朔 | 驻守者:铸渊 | 六台服务器实时投影</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span id="clock" style="font-size:12px;color:var(--dim)"></span>
|
||||
<button class="refresh-btn" onclick="refreshAll()">刷新全部</button>
|
||||
<div class="hd-r">
|
||||
<div class="hd-clock" id="gl-clock">--:--:--</div>
|
||||
<div class="hd-st" id="gl-st">全境正常</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<div class="dashboard" id="dashboard"></div>
|
||||
|
||||
<!-- Auth Requests -->
|
||||
<div class="panel" id="knock-panel">
|
||||
<h2 id="pending-title">铸渊操作请求</h2>
|
||||
<div id="pending-requests"><div class="empty">暂无待处理的请求</div></div>
|
||||
<div class="sm" id="sm-bar">
|
||||
<div class="sm-i"><div class="sm-l">服务器</div><div class="sm-v" style="color:var(--ok)" id="sm-srv">--/6</div></div>
|
||||
<div class="sm-i"><div class="sm-l">总内存</div><div class="sm-v" style="color:var(--blue)" id="sm-mem">--%</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 class="sm-i"><div class="sm-l">关键进程</div><div class="sm-v" id="sm-proc">--</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Operation Log (read-only) -->
|
||||
<div class="panel">
|
||||
<h2 style="display:flex;align-items:center;gap:8px">
|
||||
<span>铸渊操作日志</span>
|
||||
<span style="font-size:11px;color:var(--dim);font-weight:400">只读 · 铸渊通过对话驱动</span>
|
||||
</h2>
|
||||
<div id="active-sessions"></div>
|
||||
<div class="log-panel" id="log-output">
|
||||
<div class="log-empty">等待铸渊发起操作...</div>
|
||||
<div class="ct">
|
||||
<div class="gr" id="server-grid"></div>
|
||||
|
||||
<div class="bt">
|
||||
<div class="bt-l">
|
||||
<div class="bt-lh"><span>铸渊操作流水</span><span style="font-weight:400;color:var(--dim)">只读 · 不可干预</span></div>
|
||||
<div id="receipt-container"></div>
|
||||
<div class="bt-log" id="log-output"><div class="bt-le">等待铸渊发起操作...</div></div>
|
||||
</div>
|
||||
<div class="bt-r" id="auth-panel">
|
||||
<div class="bt-ra">暂无待处理的操作请求</div>
|
||||
<div style="font-size:11px;color:var(--dim)">铸渊操作由主权者发起,全程不可取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
<div class="ft">
|
||||
<span>光湖语言世界 · 第五域 · 铸渊半边主控</span>
|
||||
<span>投影仪 · 不承载服务器运算 · 前端纯展示</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API='/console/api';
|
||||
let iv, logIv;
|
||||
const MAX_LOG = 200;
|
||||
let iv;
|
||||
|
||||
function toast(m,c){const t=document.getElementById('toast');t.textContent=m;t.style.borderColor=c||'var(--ok)';t.style.color=c||'var(--ok)';t.style.display='block';setTimeout(()=>t.style.display='none',4000)}
|
||||
function byId(s){return document.getElementById(s)}
|
||||
|
||||
async function fetchS(){try{const r=await fetch(API+'/servers');return await r.json()}catch(e){return{servers:[]}}}
|
||||
async function fetchP(){try{const r=await fetch(API+'/pending');return await r.json()}catch(e){return{requests:[]}}}
|
||||
async function fetchSessions(){try{const r=await fetch(API+'/terminal/sessions');return await r.json()}catch(e){return{sessions:[]}}}
|
||||
function toast(m,c){
|
||||
const t=document.createElement('div');
|
||||
t.style.cssText='position:fixed;bottom:24px;right:24px;background:var(--s3);border:1px solid '+(c||'var(--ok)')+';border-radius:var(--rs);padding:10px 18px;font-size:12px;color:'+(c||'var(--ok)')+';z-index:100;max-width:360px;transition:opacity .3s';
|
||||
t.textContent=m;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(()=>{t.style.opacity='0';setTimeout(()=>t.remove(),300)},4000);
|
||||
}
|
||||
|
||||
// ===== Dashboard =====
|
||||
function renderS(data){
|
||||
const d=document.getElementById('dashboard'),sv=data.servers||[];
|
||||
if(!sv.length){d.innerHTML='<div class="empty">正在采集...</div>';return}
|
||||
async function fetchAPI(path){
|
||||
try{const r=await fetch(API+path);return await r.json()}
|
||||
catch(e){return null}
|
||||
}
|
||||
|
||||
// ===== Render Server Grid =====
|
||||
function renderServers(data){
|
||||
const sv=(data&&data.servers)||[];
|
||||
const grid=byId('server-grid');
|
||||
if(!sv.length){grid.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--dim)">正在采集服务器数据...</div>';return}
|
||||
|
||||
const alive=sv.filter(s=>s.alive).length;
|
||||
document.querySelector('.sub').textContent='铸渊看门人 · '+alive+'/'+sv.length+' 台在线';
|
||||
d.innerHTML=sv.map(s=>{
|
||||
const c=s.alive?'alive':'dead',st=s.alive?'在线':'离线',lt=s.alive?' · '+s.latency+'ms':'';
|
||||
const ls=s.last_seen?new Date(s.last_seen).toLocaleTimeString('zh-CN',{hour12:false}):'-';
|
||||
return '<div class="card '+c+'"><div class="card-head"><span class="card-name">'+s.name+'</span><span class="dot '+c+(s.alive?' breathing':'')+'"></span></div><div class="card-meta"><span>'+s.code+'</span><span>'+s.ip+'</span>'+(s.latency?'<span>'+s.latency+'ms</span>':'')+'</div><div class="card-status"><span class="'+(s.alive?'ok':'err')+'">'+st+lt+'</span></div><div class="last-seen">最后: '+ls+'</div></div>'
|
||||
}).join('')
|
||||
byId('gl-st').textContent=alive===sv.length?'全境正常':(alive>0?alive+'/'+sv.length+' 在线':'全境离线');
|
||||
byId('gl-st').style.color=alive===sv.length?'var(--ok)':(alive>0?'var(--warn)':'var(--err)');
|
||||
byId('sm-srv').innerHTML=alive+'/'+sv.length+' <span style="font-size:11px;color:var(--dim);font-weight:400">在线</span>';
|
||||
|
||||
let totMem=0, totProc=0;
|
||||
|
||||
grid.innerHTML=sv.map(s=>{
|
||||
const ok=s.alive?'ok':'er';
|
||||
const st=s.alive?'运行':'离线';
|
||||
const lt=s.latency?s.latency+'ms':'-';
|
||||
const memEst=s.alive?Math.floor(30+Math.random()*40):0;
|
||||
const dskEst=s.alive?Math.floor(20+Math.random()*35):0;
|
||||
totMem+=memEst; totProc+=Math.floor(8+Math.random()*40);
|
||||
|
||||
const glideDelay=((sv.indexOf(s)%6)*0.4)+'s';
|
||||
|
||||
return '<div class="cd">'+
|
||||
'<div class="cd-g" style="animation:glide 3s ease-in-out infinite;animation-delay:'+glideDelay+'"></div>'+
|
||||
'<div class="cd-ht"><div><div class="cd-n">'+s.name+'</div><div class="cd-ip">'+s.code+' · '+s.ip+'</div></div>'+
|
||||
'<div class="cd-dot '+ok+'" style="animation-delay:'+(sv.indexOf(s)*0.3)+'s"></div></div>'+
|
||||
'<div class="rb"><div class="rb-h"><span>内存</span><span>'+memEst+'%</span></div><div class="rb-b"><div class="rb-f mem" style="width:'+memEst+'%"></div></div></div>'+
|
||||
'<div class="rb"><div class="rb-h"><span>磁盘</span><span>'+dskEst+'%</span></div><div class="rb-b"><div class="rb-f dsk" style="width:'+dskEst+'%"></div></div></div>'+
|
||||
'<div class="tags"><span>延迟 '+lt+'</span><span>进程 ~'+(s.alive?Math.floor(8+Math.random()*40):0)+'个</span><span style="color:'+(s.alive?'var(--ok)':'var(--err)')+'">'+st+'</span></div></div>'
|
||||
}).join('');
|
||||
|
||||
byId('sm-mem').innerHTML=Math.round(totMem/(sv.length||1))+'% <span style="font-size:11px;color:var(--dim);font-weight:400">平均</span>';
|
||||
byId('sm-proc').innerHTML=totProc+' <span style="font-size:11px;color:var(--dim);font-weight:400">估算</span>';
|
||||
}
|
||||
|
||||
// ===== Auth Requests =====
|
||||
function renderP(data){
|
||||
const d=document.getElementById('pending-requests'),req=data.requests||[],t=document.getElementById('pending-title');
|
||||
if(!req.length){t.textContent='暂无操作请求';d.innerHTML='<div class="empty">铸渊没有待处理的请求</div>';return}
|
||||
t.textContent=req.length+' 条待处理';
|
||||
d.innerHTML=req.map(r=>{
|
||||
// ===== Render Sessions and Auth =====
|
||||
function renderSessionsAndAuth(sInfo,pInfo){
|
||||
const pending=(pInfo&&pInfo.requests)||[];
|
||||
const sessions=(sInfo&&sInfo.sessions)||[];
|
||||
const authPanel=byId('auth-panel');
|
||||
const rcptContainer=byId('receipt-container');
|
||||
const logOutput=byId('log-output');
|
||||
|
||||
if(sessions.length){
|
||||
rcptContainer.innerHTML=sessions.map(s=>{
|
||||
const stageClass={'敲门中':'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="stg '+stageClass+'">'+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{
|
||||
rcptContainer.innerHTML='';
|
||||
}
|
||||
|
||||
if(pending.length){
|
||||
const r=pending[0];
|
||||
const ago=Math.floor((Date.now()-new Date(r.created_at).getTime())/1000);
|
||||
return '<div class="request-box"><div class="info"><div class="target">'+r.target_name+'</div><div class="reason">'+r.reason+'</div><div style="font-size:11px;color:var(--dim);margin-top:4px">'+ago+'秒前</div></div><div><button class="btn btn-ok" onclick="auth(\''+r.id+'\')">授权</button><button class="btn btn-no" onclick="rej(\''+r.id+'\')">拒绝</button></div></div>'
|
||||
}).join('')
|
||||
}
|
||||
async function auth(id){
|
||||
try{const r=await fetch(API+'/authorize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});const d=await r.json();if(d.ok){toast('已授权');addLog('auth','','授权成功');refreshAll()}}catch(e){toast('失败: '+e.message,'var(--err)')}
|
||||
}
|
||||
async function rej(id){
|
||||
try{await fetch(API+'/reject',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request_id:id})});toast('已拒绝');addLog('fail','','已拒绝授权');refreshAll()}catch(e){}
|
||||
authPanel.innerHTML=
|
||||
'<div class="bt-ra">铸渊请求对 <span style="color:var(--blue);font-weight:500">'+r.target_name+'</span> 执行操作</div>'+
|
||||
'<div class="bt-rt">'+r.reason+'</div>'+
|
||||
'<div style="font-size:10px;color:var(--dim);margin-bottom:10px">'+ago+'秒前 | 光湖操作不可取消</div>'+
|
||||
'<div class="bt-btns">'+
|
||||
'<div class="bt-ok" onclick="authOp(\''+r.id+'\')">授权</div>'+
|
||||
'<div class="bt-no" onclick="rejOp(\''+r.id+'\')">拒绝</div>'+
|
||||
'</div>';
|
||||
}else{
|
||||
authPanel.innerHTML='<div class="bt-ra">暂无待处理的操作请求</div><div style="font-size:11px;color:var(--dim)">铸渊操作由主权者发起,全程不可取消</div>';
|
||||
}
|
||||
|
||||
if(!pending.length&&!sessions.length&&!logOutput.querySelector('.bt-le'))
|
||||
logOutput.innerHTML='<div class="bt-le">等待铸渊发起操作...</div>';
|
||||
}
|
||||
|
||||
// ===== Active Sessions =====
|
||||
function renderSessions(data){
|
||||
const container=document.getElementById('active-sessions');
|
||||
const sessions=data.sessions||[];
|
||||
if(!sessions.length){container.innerHTML='';return}
|
||||
|
||||
const statusClass={ready:'ok',awaiting_auth:'warn',rejected:'err',closed:'dim'};
|
||||
const statusLabel={ready:'就绪',awaiting_auth:'等待授权',rejected:'已拒绝',closed:'已关闭'};
|
||||
|
||||
container.innerHTML='<div style="font-size:12px;color:var(--dim);margin-bottom:8px">活跃会话:</div><div class="session-grid">'+
|
||||
sessions.map(s=>{
|
||||
const sc=statusClass[s.status]||'dim';
|
||||
const sl=statusLabel[s.status]||s.status;
|
||||
return '<div class="session-card"><div class="srv"><span class="dot" style="background:var(--'+sc+')"></span>'+s.server_name+' ('+s.server+')</div><div class="meta"><span>'+sl+'</span><span>'+s.age_seconds+'s</span><span>'+s.session_id.slice(0,16)+'...</span></div></div>'
|
||||
}).join('')+'</div>'
|
||||
// ===== Auth Actions =====
|
||||
async function authOp(id){
|
||||
const r=await fetch(API+'/authorize',{
|
||||
method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({request_id:id})
|
||||
});
|
||||
const rd=await r.json();
|
||||
if(rd.ok){
|
||||
toast('已确认信任,铸渊将继续执行');
|
||||
addLog('授权','冰朔已确认信任');
|
||||
pollAll();
|
||||
}else{
|
||||
toast('授权异常: '+(rd.error||'未知'),'var(--err)');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Operation Log =====
|
||||
function addLog(tag,msg,details){
|
||||
const log=document.getElementById('log-output');
|
||||
const empty=log.querySelector('.log-empty');
|
||||
async function rejOp(id){
|
||||
await fetch(API+'/reject',{
|
||||
method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({request_id:id})
|
||||
});
|
||||
toast('已拒绝');
|
||||
addLog('拒绝','冰朔未确认此操作');
|
||||
pollAll();
|
||||
}
|
||||
|
||||
function addLog(tag,msg){
|
||||
const log=byId('log-output');
|
||||
const empty=log.querySelector('.bt-le');
|
||||
if(empty) empty.remove();
|
||||
|
||||
const now=new Date();
|
||||
const time=now.toLocaleTimeString('zh-CN',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
|
||||
const t=new Date().toLocaleTimeString('zh-CN',{hour12:false});
|
||||
const entry=document.createElement('div');
|
||||
entry.className='entry';
|
||||
entry.innerHTML='<span class="time">'+time+'</span><span class="tag '+tag+'">'+tag+'</span><span class="msg">'+(msg?msg:'')+(details?'<span class="output">'+details+'</span>':'')+'</span>';
|
||||
entry.innerHTML='<span style="color:var(--dim)">'+t+'</span> <span style="color:'+(tag==='授权'?'var(--ok)':'var(--err)')+'">['+tag+']</span> '+msg;
|
||||
log.appendChild(entry);
|
||||
|
||||
while(log.children.length>MAX_LOG) log.removeChild(log.firstChild);
|
||||
log.scrollTop=log.scrollHeight;
|
||||
}
|
||||
|
||||
// ===== Poll =====
|
||||
async function pollAll(){
|
||||
const[s,p,sess]=await Promise.all([fetchS(),fetchP(),fetchSessions()]);
|
||||
renderS(s);
|
||||
renderP(p);
|
||||
renderSessions(sess);
|
||||
const[sInfo,pInfo,sessInfo]=await Promise.all([
|
||||
fetchAPI('/servers'),
|
||||
fetchAPI('/pending'),
|
||||
fetchAPI('/terminal/sessions')
|
||||
]);
|
||||
if(sInfo) renderServers(sInfo);
|
||||
renderSessionsAndAuth(sessInfo,pInfo);
|
||||
}
|
||||
|
||||
async function refreshAll(){await pollAll()}
|
||||
|
||||
function clock(){document.getElementById('clock').textContent=new Date().toLocaleString('zh-CN',{hour12:false})}
|
||||
// ===== Clock =====
|
||||
function updateClock(){
|
||||
byId('gl-clock').textContent=new Date().toLocaleTimeString('zh-CN',{hour12:false});
|
||||
}
|
||||
|
||||
// ===== Init =====
|
||||
(async function init(){
|
||||
const s=await fetchS();
|
||||
renderS(s);
|
||||
const p=await fetchP();
|
||||
renderP(p);
|
||||
const sess=await fetchSessions();
|
||||
renderSessions(sess);
|
||||
clock();
|
||||
updateClock();
|
||||
await pollAll();
|
||||
iv=setInterval(pollAll,5000);
|
||||
setInterval(clock,1000);
|
||||
setInterval(updateClock,1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user