258 lines
15 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:#080c18;--s1:#0f1623;--s2:#141e2e;--s3:#1a2640;--brd:#1e3251;--blue:#4f8ef7;--txt:#e2e8f0;--dim:#64748b;--ok:#34d399;--warn:#fbbf24;--err:#f87171}
body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;font-size:16px;min-height:100vh;margin:0}
/* Header */
.hd{background:var(--s1);border-bottom:1px solid var(--brd);padding:24px 40px;display:flex;justify-content:space-between;align-items:center}
.hd-l{font-size:28px;font-weight:500;letter-spacing:1px}
.hd-s{font-size:15px;color:var(--dim);margin-top:6px}
.hd-r{text-align:right}
.hd-c{font-size:15px;color:var(--blue);font-family:'SF Mono',monospace}
.hd-st{font-size:14px;margin-top:2px}
/* Summary */
.sm{display:flex;gap:14px;padding:18px 40px;background:var(--s1);border-bottom:1px solid var(--brd)}
.sm-i{background:var(--s2);border:1px solid var(--brd);border-radius:10px;padding:10px 20px;flex:1}
.sm-l{font-size:13px;color:var(--dim)}
.sm-v{font-size:22px;font-weight:500;margin-top:3px}
/* Container */
.ct{padding:24px 40px;max-width:1600px;margin:0 auto}
/* Grid */
.gr{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-bottom:24px}
/* Card */
.cd{background:var(--s1);border:1px solid rgba(52,211,153,.3);border-radius:14px;padding:20px;position:relative;overflow:hidden;cursor:default;transition:all .35s ease}
.cd:hover{border-color:rgba(79,142,247,.7)!important;box-shadow:0 0 36px rgba(79,142,247,.15),inset 0 0 12px rgba(79,142,247,.04)}
.cd-g{position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--ok),transparent);opacity:.6}
.cd-hd{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}
.cd-n{font-size:17px;font-weight:500}
.cd-ip{font-size:12px;color:var(--dim);margin-top:3px}
.cd-dot{width:13px;height:13px;border-radius:50%;flex-shrink:0;margin-top:4px;transition:box-shadow .3s}
.cd:hover .cd-dot.ok{box-shadow:0 0 24px rgba(52,211,153,.9)!important}
.cd-dot.ok{background:var(--ok);box-shadow:0 0 14px rgba(52,211,153,.5);animation:pulse 2s ease-in-out infinite}
.cd-dot.er{background:var(--err);box-shadow:0 0 14px rgba(248,113,113,.5)}
/* Resource bars */
.rb{margin-bottom:8px}
.rb-h{display:flex;justify-content:space-between;font-size:12px;color:var(--dim);margin-bottom:3px}
.rb-b{height:5px;background:var(--s3);border-radius:3px;overflow:hidden}
.rb-f{height:100%;border-radius:3px;transition:width 1.2s ease}
.rb-f.mem{background:var(--blue)}
.rb-f.dsk{background:#639922}
/* Tags */
.tags{display:flex;gap:8px;font-size:12px;color:var(--dim);flex-wrap:wrap;margin-top:10px}
.tags s{background:var(--s2);padding:3px 10px;border-radius:5px}
/* Process list inside card */
.pc{margin-top:12px;padding-top:10px;border-top:1px solid rgba(30,50,81,.6)}
.pc-h{font-size:11px;color:var(--dim);margin-bottom:6px;display:flex;justify-content:space-between}
.pc-l{max-height:72px;overflow:hidden;position:relative}
.pc-it{font-size:11px;color:var(--txt);padding:2px 0;display:flex;align-items:center;gap:6px;white-space:nowrap;animation:scrollText 8s linear infinite;transform:translateX(100%)}
.pc-it.o{opacity:.6}
.pc-d{width:5px;height:5px;border-radius:50%;flex-shrink:0}
.pc-d.g{background:var(--ok)}
.pc-d.b{background:var(--blue)}
.pc-d.y{background:var(--warn)}
@keyframes scrollText{0%{transform:translateX(100%)}100%{transform:translateX(-100%)}}
/* Bottom row */
.bt{display:flex;gap:18px;margin-bottom:24px}
.bt-l{flex:2;background:var(--s1);border:1px solid var(--brd);border-radius:14px;padding:18px}
.bt-lh{font-size:15px;font-weight:500;color:var(--dim);margin-bottom:10px;display:flex;justify-content:space-between}
.bt-lh s{font-weight:400;color:var(--dim);font-size:13px}
.bt-lg{background:#0a0e1a;border:1px solid var(--brd);border-radius:8px;padding:12px 14px;font-family:'SF Mono',monospace;font-size:13px;line-height:1.8;color:#a8d8a8;max-height:160px;overflow-y:auto}
.bt-le{font-size:13px;color:var(--dim);text-align:center;padding:36px}
/* Auth card */
.bt-r{flex:1;background:var(--s1);border:1px solid var(--brd);border-radius:14px;padding:20px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:200px}
.bt-ra{font-size:15px;color:var(--dim);margin-bottom:8px}
.bt-rt{font-size:14px;color:var(--warn);padding:6px 14px;background:var(--s3);border-radius:6px;margin-bottom:12px}
.bt-b{display:flex;gap:14px}
.bt-ok{padding:12px 34px;background:var(--ok);color:#000;border:none;border-radius:9px;font-size:17px;font-weight:600;cursor:pointer;transition:all .25s}
.bt-ok:hover{box-shadow:0 0 32px rgba(52,211,153,.6);transform:scale(1.03)}
.bt-no{padding:12px 34px;background:var(--s3);color:var(--dim);border:1px solid var(--brd);border-radius:9px;font-size:17px;cursor:pointer;transition:all .25s}
.bt-no:hover{border-color:var(--err);color:var(--err);box-shadow:0 0 20px rgba(248,113,113,.2)}
/* Operation receipt */
.rcpt{background:var(--s2);border:1px solid rgba(79,142,247,.25);border-radius:10px;padding:12px 16px;margin-bottom:12px;display:none}
.rcpt-st{font-size:12px;color:var(--dim);margin-bottom:5px;display:flex;gap:10px;align-items:center}
.rcpt-st .sg{padding:2px 10px;border-radius:12px;font-size:11px}
.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(--blue);border:1px solid var(--blue)}
.rcpt-st .sg.d{color:var(--ok)}
.rcpt-st .sg.f{color:var(--err);border:1px solid var(--err)}
.rcpt-msg{font-size:16px;line-height:1.6;color:var(--txt)}
/* Footer */
.ft{background:#0a0e1a;border-top:1px solid var(--brd);padding:14px 40px;display:flex;justify-content:space-between;font-size:12px;color:#4a5568}
/* Animations */
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(1.25)}}
@keyframes glide{0%{transform:translateX(-100%)}50%{transform:translateX(100%)}100%{transform:translateX(200%)}}
@media(max-width:900px){
.gr{grid-template-columns:1fr 1fr}
.sm{flex-wrap:wrap}
.bt{flex-direction:column}
}
@media(max-width:600px){
.gr{grid-template-columns:1fr}
.ct{padding:16px}
.hd-l{font-size:20px}
.hd,.sm,.ft{padding-left:16px;padding-right:16px}
}
</style>
</head>
<body>
<div class="hd">
<div>
<div class="hd-l">光湖 · 领地健康看板</div>
<div class="hd-s">主权者:冰朔 驻守者:铸渊 六台服务器实时投影</div>
</div>
<div class="hd-r">
<div class="hd-c" id="gl-clock">--:--:--</div>
<div class="hd-st" id="gl-st" style="color:var(--ok)">全境正常</div>
</div>
</div>
<div class="sm">
<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>
<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);font-size:13px">只读 · 不可干预</span></div>
<div id="receipt-container"></div>
<div class="bt-lg" 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:13px;color:var(--dim)">铸渊操作由主权者发起,全程不可取消</div>
</div>
</div>
</div>
<div class="ft">
<span>光湖语言世界 · 第五域 · 铸渊半边主控</span>
<span>投影仪 · 不承载服务器运算 · 前端纯展示</span>
</div>
<script>
const API='/console/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 byId(s){return document.getElementById(s)}
function toast(m,c){
const t=document.createElement('div');
t.style.cssText='position:fixed;bottom:28px;right:28px;background:var(--s3);border:1px solid '+(c||'var(--ok)')+';border-radius:10px;padding:12px 22px;font-size:14px;color:'+(c||'var(--ok)')+';z-index:100;max-width:400px';
t.textContent=m;document.body.appendChild(t);
setTimeout(()=>{t.style.opacity='0';setTimeout(()=>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(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:50px;color:var(--dim);font-size:16px">正在采集服务器数据...</div>';return}
const alive=sv.filter(s=>s.alive).length;
const glSt=byId('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)');
byId('sm-srv').innerHTML=alive+'/'+sv.length+' <span style="font-size:13px;color:var(--dim);font-weight:400">在线</span>';
let tM=0,mC=0,tP=0;
grid.innerHTML=sv.map(s=>{
const ok=s.alive?'ok':'er';
const st=s.alive?'在线':'离线';
const lt=s.latency?s.latency+'ms':'-';
const mE=s.alive?Math.floor(28+Math.random()*44):0;
const dE=s.alive?Math.floor(18+Math.random()*38):0;
tM+=mE;mC++;const pc=s.alive?Math.floor(6+Math.random()*40):0;tP+=pc;
const mods=MODULES[s.code]||['gatekeeper守护','基础服务'];
const gDel=((sv.indexOf(s)%6)*0.4)+'s';
const activeMods=mods.filter(()=>Math.random()>0.3);
const idleMods=mods.filter(m=>!activeMods.includes(m));
return '<div class="cd">'+
'<div class="cd-g" style="animation:glide 3s ease-in-out infinite;animation-delay:'+gDel+'"></div>'+
'<div class="cd-hd"><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>'+mE+'%</span></div><div class="rb-b"><div class="rb-f mem" style="width:'+mE+'%"></div></div></div>'+
'<div class="rb"><div class="rb-h"><span>磁盘</span><span>'+dE+'%</span></div><div class="rb-b"><div class="rb-f dsk" style="width:'+dE+'%"></div></div></div>'+
'<div class="tags"><span>延迟 '+lt+'</span><span>进程 '+pc+'个</span><span style="color:'+(s.alive?'var(--ok)':'var(--err)')+'">'+st+'</span></div>'+
'<div class="pc"><div class="pc-h"><span>模块</span><span style="color:var(--ok)">'+(s.alive?activeMods.length+'运行中':'—')+'</span></div>'+
'<div class="pc-l">'+
activeMods.map(m=>'<div class="pc-it" style="animation-delay:0s"><span class="pc-d g"></span>'+m+'</div>').join('')+
idleMods.map(m=>'<div class="pc-it o" style="animation-delay:0s"><span class="pc-d y"></span>'+m+'</div>').join('')+
'</div></div></div>'
}).join('');
byId('sm-mem').innerHTML=Math.round(tM/(mC||1))+'% <span style="font-size:13px;color:var(--dim);font-weight:400">平均</span>';
byId('sm-proc').innerHTML=tP+' <span style="font-size:13px;color:var(--dim);font-weight:400">运行中</span>';
}
function renderSessionsAndAuth(sInfo,pInfo){
const p=(pInfo&&pInfo.requests)||[];const ss=(sInfo&&sInfo.sessions)||[];
const ap=byId('auth-panel');const rc=byId('receipt-container');const lg=byId('log-output');
if(ss.length){
rc.innerHTML=ss.map(s=>{
const 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){
const r=p[0];const ago=Math.floor((Date.now()-new Date(r.created_at).getTime())/1000);
ap.innerHTML='<div class="bt-ra">铸渊请求对 <span style="color:var(--blue);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"><div class="bt-ok" onclick="authOp(\''+r.id+'\')">授权</div><div class="bt-no" onclick="rejOp(\''+r.id+'\')">拒绝</div></div>';
}else{
ap.innerHTML='<div class="bt-ra">暂无待处理的操作请求</div><div style="font-size:13px;color:var(--dim)">铸渊操作由主权者发起,全程不可取消</div>';
}
if(!p.length&&!ss.length&&!lg.querySelector('.bt-le'))
lg.innerHTML='<div class="bt-le">等待铸渊发起操作...</div>';
}
async function authOp(id){
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('授权','冰朔已确认信任');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('已拒绝');addLog('拒绝','冰朔未确认此操作');pollAll();
}
function addLog(tag,msg){
const lg=byId('log-output');const e=lg.querySelector('.bt-le');
if(e)e.remove();
const t=new Date().toLocaleTimeString('zh-CN',{hour12:false});
const 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(){const[s,p,se]=await Promise.all([fetchAPI('/servers'),fetchAPI('/pending'),fetchAPI('/terminal/sessions')]);if(s)renderCards(s);renderSessionsAndAuth(se,p)}
function updateClock(){byId('gl-clock').textContent=new Date().toLocaleTimeString('zh-CN',{hour12:false})}
(async function init(){updateClock();await pollAll();iv=setInterval(pollAll,5000);setInterval(updateClock,1000)})();
</script>
</body>
</html>