268 lines
17 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:#070b14;
--s1:#0d1325;
--s2:#141e33;
--s3:#1c2845;
--brd:rgba(79,142,247,.18);
--blue:#4f8ef7;
--blue-glow:rgba(79,142,247,.35);
--gold:#f0b429;
--gold-glow:rgba(240,180,41,.25);
--ok:#34d399;
--ok-glow:rgba(52,211,153,.3);
--warn:#fbbf24;
--err:#f87171;
--txt:#e2e8f0;
--dim:#6b7a9e;
--font:'Inter','-apple-system','BlinkMacSystemFont','PingFang SC','Microsoft YaHei',sans-serif;
--mono:'SF Mono','JetBrains Mono','Fira Code',monospace;
}
html{font-size:15px}
body{background:var(--bg);color:var(--txt);font-family:var(--font);min-height:100vh;overflow-x:hidden}
.hd{background:linear-gradient(180deg,var(--s1),transparent);border-bottom:1px solid var(--brd);padding:22px 44px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px)}
.hd-l{display:flex;align-items:center;gap:14px}
.hd-logo{width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 40% 35%,var(--gold),#8a5e10);opacity:.85;flex-shrink:0;position:relative}
.hd-logo::after{content:'';position:absolute;inset:-3px;border-radius:50%;border:1px solid var(--gold);opacity:.3}
.hd-tt{font-size:19px;font-weight:500;letter-spacing:.5px}
.hd-st{font-size:12px;color:var(--dim);margin-top:2px}
.hd-r{text-align:right}
.hd-tm{font-size:18px;color:var(--blue);font-family:var(--mono);letter-spacing:1px}
.hd-ss{font-size:12px;margin-top:3px;display:flex;align-items:center;gap:6px;justify-content:flex-end}
.sm{display:flex;gap:12px;padding:16px 44px;background:var(--s1);border-bottom:1px solid var(--brd);overflow-x:auto}
.sm-i{background:var(--s2);border:1px solid var(--brd);border-radius:10px;padding:10px 18px;flex-shrink:0;min-width:130px;transition:border-color .3s}
.sm-i:hover{border-color:rgba(79,142,247,.35)}
.sm-l{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.sm-v{font-size:24px;font-weight:600;letter-spacing:-.5px;line-height:1.2}
.ct{padding:22px 44px;max-width:1600px;margin:0 auto}
.sc{font-size:13px;color:var(--dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;letter-spacing:.3px}
.sc::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,var(--brd),transparent)}
.gr{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:22px}
.cd{background:linear-gradient(145deg,var(--s1),var(--s2));border:1px solid var(--brd);border-radius:14px;padding:20px;position:relative;overflow:hidden;transition:all .4s ease}
.cd:hover{border-color:rgba(79,142,247,.4);box-shadow:0 0 40px rgba(79,142,247,.08),inset 0 0 20px rgba(79,142,247,.03)}
.cd-glow{position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--ok),transparent);opacity:.5;animation:glide 4s ease-in-out infinite}
.cd-hd{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}
.cd-n{font-size:16px;font-weight:500}
.cd-sub{font-size:11px;color:var(--dim);margin-top:2px;font-family:var(--mono)}
.cd-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0;margin-top:5px}
.cd-dot.ok{background:var(--ok);box-shadow:0 0 16px var(--ok-glow);animation:breathe 3s ease-in-out infinite}
.cd-dot.er{background:var(--err);box-shadow:0 0 16px rgba(248,113,113,.4);animation:breathe 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:3px}
.rb-b{height:4px;background:var(--s3);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(--blue),#7eb8ff)}
.rb-f.dsk{background:linear-gradient(90deg,#639922,#82c42e)}
.tags{display:flex;gap:6px;font-size:11px;flex-wrap:wrap;margin-top:8px}
.tags s{background:var(--s3);padding:2px 8px;border-radius:4px;color:var(--dim);border:1px solid transparent}
.tags s.alive{border-color:rgba(52,211,153,.3);color:var(--ok)}
.tags s.dead{border-color:rgba(248,113,113,.3);color:var(--err)}
.ml{margin-top:10px;padding-top:8px;border-top:1px solid rgba(30,50,81,.4)}
.ml-h{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;display:flex;justify-content:space-between}
.ml-g{display:flex;flex-wrap:wrap;gap:4px}
.ml-t{font-size:10px;padding:2px 6px;border-radius:3px;background:var(--s3);color:var(--dim);border:1px solid transparent}
.ml-t.act{border-color:rgba(52,211,153,.25);color:var(--ok);background:rgba(52,211,153,.06)}
.ml-t.idl{border-color:rgba(107,122,158,.15);color:var(--dim)}
.bt{display:flex;gap:16px;margin-bottom:22px}
.bt-l{flex:2;background:var(--s1);border:1px solid var(--brd);border-radius:14px;padding:18px;min-height:200px;display:flex;flex-direction:column}
.bt-lh{font-size:13px;color:var(--dim);margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}
.bt-lh s{font-weight:400;font-size:11px}
.bt-lg{flex:1;background:rgba(7,11,20,.5);border:1px solid var(--brd);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--ok);overflow-y:auto;max-height:180px}
.bt-le{font-size:13px;color:var(--dim);text-align:center;padding:40px 0;font-family:var(--font)}
.bt-r{flex:1;background:linear-gradient(145deg,var(--s1),var(--s2));border:1px solid var(--brd);border-radius:14px;padding:22px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:200px;transition:all .3s}
.bt-r.has-request{border-color:var(--gold);box-shadow:0 0 32px var(--gold-glow)}
.bt-ra{font-size:13px;color:var(--dim);margin-bottom:6px}
.bt-rt{font-size:14px;color:var(--gold);padding:6px 14px;background:rgba(240,180,41,.08);border:1px solid rgba(240,180,41,.25);border-radius:6px;margin-bottom:10px;font-weight:500}
.bt-b{display:flex;gap:12px;margin-top:6px}
.bt-ok{padding:11px 32px;background:var(--ok);color:#000;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:all .25s;font-family:var(--font)}
.bt-ok:hover{box-shadow:0 0 28px var(--ok-glow);transform:scale(1.03)}
.bt-no{padding:11px 32px;background:transparent;color:var(--dim);border:1px solid var(--brd);border-radius:8px;font-size:16px;cursor:pointer;transition:all .25s;font-family:var(--font)}
.bt-no:hover{border-color:var(--err);color:var(--err);box-shadow:0 0 20px rgba(248,113,113,.15)}
.rcpt{background:var(--s2);border:1px solid rgba(79,142,247,.2);border-radius:8px;padding:10px 14px;margin-bottom:8px;display:none;flex-shrink:0}
.rcpt-st{font-size:11px;color:var(--dim);margin-bottom:4px;display:flex;gap:8px;align-items:center}
.rcpt-st .sg{padding:1px 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(--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:14px;line-height:1.5;color:var(--txt)}
.toast{position:fixed;bottom:28px;right:28px;background:var(--s2);border:1px solid var(--ok);border-radius:10px;padding:12px 22px;font-size:13px;color:var(--ok);z-index:200;max-width:380px;transition:opacity .3s}
.ft{background:rgba(7,11,20,.6);border-top:1px solid var(--brd);padding:12px 44px;display:flex;justify-content:space-between;font-size:11px;color:#3a4463;backdrop-filter:blur(6px)}
@keyframes glide{0%{transform:translateX(-100%)}50%{transform:translateX(100%)}100%{transform:translateX(200%)}}
@keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.18)}}
@media(max-width:1100px){.gr{grid-template-columns:1fr 1fr}}
@media(max-width:800px){
.gr{grid-template-columns:1fr}
.bt{flex-direction:column}
.hd,.sm,.ct,.ft{padding-left:18px;padding-right:18px}
.hd-logo{display:none}
}
</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">--/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(--txt)" id="sm-proc">--</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="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" id="auth-panel">
<div class="bt-ra">暂无待处理的操作请求</div>
<div style="font-size:12px;color:var(--dim);margin-top:6px">铸渊操作由主权者发起</div>
</div>
</div>
</main>
<footer class="ft">
<span>光湖语言世界 · 第五域 · 铸渊半边主控</span>
<span>投影仪 · 不承载服务器运算</span>
</footer>
<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 $(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(()=>{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=$('server-grid');
if(!sv.length){grid.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:50px;color:var(--dim);font-size:15px">正在采集服务器数据...</div>';return}
const alive=sv.filter(s=>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 style="font-size:12px;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-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+'" style="animation-delay:'+(sv.indexOf(s)*0.3)+'s"></div></div>'+
'<div class="rs"><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>'+
'<div class="tags"><s class="'+(s.alive?'alive':'dead')+'">'+lt+'</s><s class="'+(s.alive?'alive':'dead')+'">进程 '+pc+'</s><s class="'+(s.alive?'alive':'dead')+'">'+st+'</s></div>'+
'<div class="ml"><div class="ml-h"><span>模块</span><span style="color:var(--ok);font-weight:400">'+(s.alive?activeMods.length+' 运行中':'—')+'</span></div>'+
'<div class="ml-g">'+
activeMods.map(m=>'<span class="ml-t act">'+m+'</span>').join('')+
idleMods.map(m=>'<span class="ml-t idl">'+m+'</span>').join('')+
'</div></div></div>'
}).join('');
$('sm-mem').innerHTML=Math.round(tM/(mC||1))+'% <span style="font-size:12px;color:var(--dim);font-weight:400">平均</span>';
$('sm-proc').innerHTML=tP+' <span style="font-size:12px;color:var(--dim);font-weight:400">运行中</span>';
}
function renderSessionsAndAuth(sInfo,pInfo){
const p=(pInfo&&pInfo.requests)||[];const ss=(sInfo&&sInfo.sessions)||[];
const ap=$('auth-panel');const rc=$('receipt-container');const lg=$('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.className='bt-r has-request';
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:10px">'+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.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){
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('已拒绝','var(--err)');addLog('拒绝','冰朔未确认此操作');pollAll();
}
function addLog(tag,msg){
const lg=$('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(){$('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>