299 lines
14 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;--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}
/* 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}
/* 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="hd">
<div>
<div class="hd-l">光湖 · 领地健康看板</div>
<div class="hd-s">主权者:冰朔 驻守者:铸渊 六台服务器实时投影</div>
</div>
<div class="hd-r">
<div class="hd-clock" id="gl-clock">--:--:--</div>
<div class="hd-st" id="gl-st">全境正常</div>
</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>
<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 class="ft">
<span>光湖语言世界 · 第五域 · 铸渊半边主控</span>
<span>投影仪 · 不承载服务器运算 · 前端纯展示</span>
</div>
<script>
const API='/console/api';
let iv;
function byId(s){return document.getElementById(s)}
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);
}
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;
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+' &middot; '+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>';
}
// ===== 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);
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>';
}
// ===== 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)');
}
}
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 t=new Date().toLocaleTimeString('zh-CN',{hour12:false});
const entry=document.createElement('div');
entry.innerHTML='<span style="color:var(--dim)">'+t+'</span> <span style="color:'+(tag==='授权'?'var(--ok)':'var(--err)')+'">['+tag+']</span> '+msg;
log.appendChild(entry);
log.scrollTop=log.scrollHeight;
}
// ===== Poll =====
async function pollAll(){
const[sInfo,pInfo,sessInfo]=await Promise.all([
fetchAPI('/servers'),
fetchAPI('/pending'),
fetchAPI('/terminal/sessions')
]);
if(sInfo) renderServers(sInfo);
renderSessionsAndAuth(sessInfo,pInfo);
}
// ===== Clock =====
function updateClock(){
byId('gl-clock').textContent=new Date().toLocaleTimeString('zh-CN',{hour12:false});
}
// ===== Init =====
(async function init(){
updateClock();
await pollAll();
iv=setInterval(pollAll,5000);
setInterval(updateClock,1000);
})();
</script>
</body>
</html>