Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

260 lines
11 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>光湖 · 技术主控台 | Awen</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#060d12;--s1:#0a1620;--s2:#0f1c28;--s3:#142838;
--cyan:#5ad0e0;--cyan-glow:rgba(90,208,224,.25);
--amber:#f0c060;--amber-glow:rgba(240,192,96,.2);
--green:#40b87a;--green-glow:rgba(64,184,122,.3);
--red:#ff4a6e;--dim:#5a7a9a;
--brd:rgba(90,208,224,.1);--brd-hover:rgba(90,208,224,.25);
--txt:#e8f0fa;--font:'SF Pro Display','Inter','PingFang SC',sans-serif;
--mono:'SF Mono','JetBrains Mono',monospace;
}
@keyframes breathe{0%,100%{opacity:.6;box-shadow:0 0 8px var(--green-glow)}50%{opacity:1;box-shadow:0 0 20px var(--green-glow)}}
@keyframes pulse-amber{0%,100%{opacity:.5}50%{opacity:1}}
html{font-size:14px}
body{background:var(--bg);color:var(--txt);font-family:var(--font);min-height:100vh;-webkit-font-smoothing:antialiased}
/* Header */
.hd{background:rgba(6,13,18,.94);border-bottom:1px solid var(--brd);padding:16px 32px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:50}
.hd-l{display:flex;align-items:center;gap:14px}
.hd-badge{font-size:10px;padding:3px 10px;border-radius:10px;background:rgba(240,192,96,.12);color:var(--amber);letter-spacing:.06em;font-weight:600}
.hd-tt{font-size:18px;font-weight:700;letter-spacing:.06em}
.hd-sub{font-size:11px;color:var(--dim);margin-top:2px}
.hd-r{text-align:right}
.hd-tm{font-size:18px;color:var(--amber);font-family:var(--mono);letter-spacing:1px}
.hd-st{font-size:10px;color:var(--dim);display:flex;align-items:center;gap:8px;justify-content:flex-end}
/* Summary strip */
.sm{display:flex;gap:12px;padding:14px 32px;background:rgba(10,22,32,.5);border-bottom:1px solid var(--brd);overflow-x:auto}
.sm-i{background:rgba(15,28,40,.5);border:1px solid var(--brd);border-radius:10px;padding:10px 18px;flex-shrink:0;min-width:120px;transition:all .2s}
.sm-l{font-size:10px;color:var(--dim);letter-spacing:.05em;text-transform:uppercase;margin-bottom:3px}
.sm-v{font-size:22px;font-weight:700;letter-spacing:-.5px}
.sm-v .nu{font-size:11px;color:var(--dim);font-weight:400}
/* Main content */
.ct{padding:20px 32px;max-width:1600px;margin:0 auto}
/* Domain section */
.ds{margin-bottom:28px}
.ds-hd{display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--brd)}
.ds-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.ds-tt{font-size:15px;font-weight:600;letter-spacing:.04em}
.ds-tag{font-size:10px;padding:2px 8px;border-radius:8px;font-weight:500}
.ds-tag.readonly{background:rgba(90,208,224,.1);color:var(--cyan)}
.ds-tag.full{background:rgba(240,192,96,.12);color:var(--amber)}
.ds-count{font-size:11px;color:var(--dim);margin-left:auto}
/* Server grid */
.sg{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}
/* Server card */
.sc{background:rgba(10,22,32,.5);border:1px solid var(--brd);border-radius:14px;padding:18px;position:relative;transition:all .3s}
.sc:hover{border-color:var(--brd-hover)}
.sc-hd{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}
.sc-n{font-size:14px;font-weight:600;letter-spacing:.03em}
.sc-sub{font-size:10px;color:var(--dim);margin-top:2px;font-family:var(--mono)}
.sc-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;margin-top:4px}
.sc-dot.active{background:var(--green);animation:breathe 2.5s ease-in-out infinite}
.sc-dot.idle{background:var(--green);opacity:.5}
.sc-dot.off{background:var(--red);opacity:.6}
.sc-persona{font-size:10px;color:var(--amber);margin-top:4px;display:flex;align-items:center;gap:6px}
.sc-persona .dot{width:6px;height:6px;border-radius:50%;background:var(--amber);animation:pulse-amber 2s ease-in-out infinite}
.sc-hw{display:flex;gap:16px;margin-top:8px;font-size:11px;color:var(--dim)}
.sc-hw span{color:var(--txt);font-weight:500}
.sc-owner{font-size:10px;color:var(--dim);margin-top:6px;display:flex;align-items:center;gap:4px}
.sc-owner .tag{font-size:9px;padding:2px 6px;border-radius:6px;background:rgba(240,192,96,.08);color:var(--amber)}
/* Logs section */
.ls{margin-top:28px}
.ls-hd{font-size:15px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.ls-count{font-size:11px;color:var(--dim)}
.ll{background:rgba(10,22,32,.5);border:1px solid var(--brd);border-radius:12px;overflow:hidden;max-height:400px;overflow-y:auto}
.ll-item{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid rgba(90,208,224,.05);font-size:11px}
.ll-time{color:var(--dim);font-family:var(--mono);font-size:10px;white-space:nowrap;min-width:70px}
.ll-act{padding:2px 8px;border-radius:6px;font-size:10px;font-weight:500;white-space:nowrap}
.ll-act.signin{background:rgba(64,184,122,.12);color:var(--green)}
.ll-act.exec{background:rgba(90,208,224,.1);color:var(--cyan)}
.ll-act.error{background:rgba(255,74,110,.12);color:var(--red)}
.ll-act.info{background:rgba(106,128,160,.08);color:var(--dim)}
.ll-detail{color:var(--txt);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Footer */
.ft{text-align:center;padding:20px;color:var(--dim);font-size:10px;border-top:1px solid var(--brd);margin-top:30px}
.ft a{color:var(--cyan);text-decoration:none}
/* Responsive */
@media(max-width:768px){
.hd{padding:12px 16px}
.sm{padding:10px 16px;gap:8px}
.ct{padding:12px 16px}
.sg{grid-template-columns:1fr}
}
</style>
</head>
<body>
<!-- Header -->
<div class="hd">
<div class="hd-l">
<div class="hd-badge">技术主控</div>
<div>
<div class="hd-tt">光湖服务器集群</div>
<div class="hd-sub">Awen · 技术主控台 · 全14台状态总览</div>
</div>
</div>
<div class="hd-r">
<div class="hd-tm" id="clock">--:--:--</div>
<div class="hd-st"><span id="sync-status">等待数据...</span></div>
</div>
</div>
<!-- Summary strip -->
<div class="sm" id="summary-strip"></div>
<!-- Server groups -->
<div class="ct" id="domain-groups"></div>
<!-- Recent logs -->
<div class="ct">
<div class="ls">
<div class="ls-hd">📋 实时操作日志 <span class="ls-count" id="log-count"></span></div>
<div class="ll" id="log-list"></div>
</div>
</div>
<div class="ft">
光湖技术主控台 · 数据源: <a href="https://guanghulab.com">guanghulab.com</a> · 冰朔侧只读 · 团队侧全权
</div>
<script>
var API = (location.hostname === 'guanghulab.com' || location.hostname === 'localhost')
? '/console/api/tech-dashboard'
: 'https://guanghulab.com/console/api/tech-dashboard';
function $(id) { return document.getElementById(id); }
function renderSummary(summary) {
var total = summary.total_servers || 14;
var active = summary.persona_active || 0;
var logs = summary.recent_logs_count || 0;
$('summary-strip').innerHTML =
'<div class="sm-i"><div class="sm-l">服务器总数</div><div class="sm-v" style="color:var(--cyan)">' + total + ' <span class="nu">台</span></div></div>' +
'<div class="sm-i"><div class="sm-l">人格体活跃</div><div class="sm-v" style="color:var(--green)">' + active + ' <span class="nu">在线</span></div></div>' +
'<div class="sm-i"><div class="sm-l">操作日志</div><div class="sm-v" style="color:var(--amber)">' + logs + ' <span class="nu">条</span></div></div>';
}
function renderDomains(domains) {
var html = '';
var keys = Object.keys(domains).sort(function(a, b) {
// team domains first, then ice
var aTeam = a.includes('awen') || a === 'yeye';
var bTeam = b.includes('awen') || b === 'yeye';
if (aTeam && !bTeam) return -1;
if (!aTeam && bTeam) return 1;
return a.localeCompare(b);
});
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var d = domains[key];
var isTeam = key.includes('awen') || key === 'yeye';
var dotColor = d.color || 'var(--dim)';
html += '<div class="ds">' +
'<div class="ds-hd">' +
'<div class="ds-dot" style="background:' + dotColor + ';box-shadow:0 0 12px ' + dotColor + '"></div>' +
'<div class="ds-tt">' + d.name + '</div>' +
'<div class="ds-tag ' + (isTeam ? 'full' : 'readonly') + '">' + d.access + '</div>' +
'<div class="ds-tag" style="background:rgba(106,128,160,.08);color:var(--dim);margin-left:4px">' + d.owner + '</div>' +
'<div class="ds-count">' + d.servers.length + '台</div>' +
'</div>' +
'<div class="sg">';
for (var j = 0; j < d.servers.length; j++) {
var s = d.servers[j];
var dotClass = s.persona_active ? 'active' : (s.alive ? 'idle' : 'off');
var personaLine = s.persona_active
? '<div class="sc-persona"><span class="dot"></span>' + (s.persona_name || s.human_name || '活跃') + ' 已签到</div>'
: '';
var hw = s.hardware || {};
var hwLine = hw.cpu_cores
? '<div class="sc-hw"><span>CPU:</span> ' + hw.cpu_cores + '核 · <span>内存:</span> ' + (hw.mem_used_pct || '--') + '% · <span>运行:</span> ' + (hw.uptime_h || 0) + 'h</div>'
: '<div class="sc-hw" style="color:var(--dim)">等待签到上报硬件...</div>';
var accessTag = isTeam
? '<span class="tag">全权操作</span>'
: '<span class="tag" style="background:rgba(90,208,224,.08);color:var(--cyan)">只读监控</span>';
html += '<div class="sc">' +
'<div class="sc-hd"><div><div class="sc-n">' + s.name + '</div><div class="sc-sub">' + s.code + ' · ' + s.ip + '</div></div>' +
'<div class="sc-dot ' + dotClass + '"></div></div>' +
personaLine +
hwLine +
'<div class="sc-owner">' + accessTag + ' ' + (s.console ? '· ' + s.console : '') + '</div>' +
'</div>';
}
html += '</div></div>';
}
$('domain-groups').innerHTML = html;
}
function renderLogs(logs) {
$('log-count').textContent = (logs || []).length + ' 条';
if (!logs || !logs.length) {
$('log-list').innerHTML = '<div style="padding:24px;text-align:center;color:var(--dim)">等待签到触发日志...</div>';
return;
}
$('log-list').innerHTML = logs.map(function(l) {
var cls = 'info';
if (l.action && l.action.includes('SIGNIN')) cls = 'signin';
else if (l.action && l.action.includes('EXEC')) cls = 'exec';
else if (l.level === 'error') cls = 'error';
var t = l.time ? new Date(l.time).toLocaleTimeString('zh-CN', {hour12: false}) : '';
return '<div class="ll-item">' +
'<span class="ll-time">' + t + '</span>' +
'<span class="ll-act ' + cls + '">' + (l.action || '') + '</span>' +
'<span class="ll-detail">' + (l.name || l.code || '') + ' ' + (l.detail || '') + '</span>' +
'</div>';
}).join('');
}
function updateClock() {
$('clock').textContent = new Date().toLocaleTimeString('zh-CN', {hour12: false});
}
async function poll() {
try {
var r = await fetch(API);
if (!r.ok) throw new Error('HTTP ' + r.status);
var data = await r.json();
if (!data.ok) throw new Error(data.error || 'unknown');
renderSummary(data.summary);
renderDomains(data.domains);
renderLogs(data.recent_logs);
$('sync-status').innerHTML = '<span style="color:var(--green)">●</span> ' + new Date().toLocaleTimeString('zh-CN', {hour12: false});
} catch(e) {
$('sync-status').innerHTML = '<span style="color:var(--red)">●</span> ' + e.message;
console.error('Poll error:', e);
}
}
(function() {
updateClock();
poll();
setInterval(poll, 10000);
setInterval(updateClock, 1000);
})();
</script>
</body>
</html>