feat: Awen技术主控台 — 全14台状态总览+域分组+操作日志

变更:
- server.js v3.7: +HL-SG-001/HL-CN-001 (14台) + /api/tech-dashboard端点
- tech.html: Awen技术主控台独立前端(琥珀色主题)
- 域分组: ice-core(冰朔8台) + awen-team(企业3) + awen-personal(Awen2) + yeye(页页1)
- 权限标识: 冰朔侧只读,团队侧全权
- 操作日志按域标记+签到可视化

数据流: tech.html → /api/tech-dashboard → signinCache+opLogs
部署: guanghulab.com/console/tech (冰朔侧) + guanghuzhiqiu.top (Awen侧)
This commit is contained in:
铸渊 ICE-GL-ZY001 2026-05-26 03:10:55 +08:00
parent ed873f850e
commit ad4c3757b9
2 changed files with 374 additions and 18 deletions

View File

@ -1,8 +1,8 @@
// # 光湖服务器主控台 API v3.6
// # Guanghu Server Console v3.6 — 12台(冰6+企3+之2+页1)
// # 光湖服务器主控台 API v3.7
// # Guanghu Server Console v3.7 — 14台(冰6+企3+之2+页1+Awen2)
// # 部署: guanghulab.com/console/ · GZ-006:3920(主) + SG-001:3920(备)
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
// # v3.6: 心跳改为人格体签到 — 唤醒签到一次,非定时监控
// # v3.7: +Awen技术主控视图 /api/tech-dashboard + HL-SG-001/HL-CN-001
const express = require('express');
const http = require('http');
@ -17,20 +17,25 @@ app.use(express.static(__dirname));
const DATA_DIR = '/opt/zhuyuan/data';
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {}
// 全服务器配置 — 冰朔6台 + 企业3台 + 之之2台 + 页页1台 = 12台
// 全服务器配置 — 14台: 冰朔6 + 企业3 + 之之2 + 页页1 + Awen个人2
const SERVERS = [
{ code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } },
{ code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 } },
{ code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 } },
{ code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 } },
{ code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } },
{ code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 } },
{ code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 } },
{ code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 } },
{ code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 } },
{ code: "ZZ-SV-001", name: "之之 · 硅谷", ip: "43.173.121.48", key: "zy_gtw_06151d40b1313956b069f3c13d0c1b8a70405ae201f5e7b2", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 59 } },
{ code: "ZZ-GZ-001", name: "之之 · 广州", ip: "193.112.126.174", key: "zy_gtw_77c779183284bf39d58080b4f5377d74ec6abc0f1f2218d6", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 } },
{ code: "YY-SV-001", name: "页页 · 硅谷", ip: "43.153.118.46", key: "zy_gtw_706430821f334302ca50d341ffe3ccc046513bbfe7f56aa3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } },
// ═══ 冰朔语言主控层 (ice-core) — 8台 ═══
{ code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 }, side: "ice", domain: "ice-core", owner: "冰朔", console: "guanghulab.com" },
{ code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 }, side: "ice", domain: "ice-core", owner: "冰朔" },
{ code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 }, side: "ice", domain: "ice-core", owner: "冰朔" },
{ code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 }, side: "ice", domain: "ice-core", owner: "冰朔" },
{ code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 }, side: "ice", domain: "ice-core", owner: "冰朔" },
{ code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 }, side: "ice", domain: "ice-core", owner: "冰朔" },
{ code: "ZZ-SV-001", name: "之之 · 硅谷", ip: "43.173.121.48", key: "zy_gtw_06151d40b1313956b069f3c13d0c1b8a70405ae201f5e7b2", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 59 }, side: "ice", domain: "zhizhi-hidden-channel", owner: "之之", persona: "DEV-004" },
{ code: "ZZ-GZ-001", name: "之之 · 广州", ip: "193.112.126.174", key: "zy_gtw_77c779183284bf39d58080b4f5377d74ec6abc0f1f2218d6", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 }, side: "ice", domain: "zhizhi-hidden-channel", owner: "之之", persona: "DEV-004" },
// ═══ 光湖团队 (Awen技术主控) — 6台 ═══
{ code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 }, side: "team", domain: "awen-team", owner: "Awen" },
{ code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 }, side: "team", domain: "awen-team", owner: "Awen" },
{ code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 }, side: "team", domain: "awen-team", owner: "Awen" },
{ code: "YY-SV-001", name: "页页 · 硅谷", ip: "43.153.118.46", key: "zy_gtw_706430821f334302ca50d341ffe3ccc046513bbfe7f56aa3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 }, side: "team", domain: "yeye", owner: "页页", persona: "ICE-GL-XTS001" },
{ code: "HL-SG-001", name: "光湖团队 · 硅谷", ip: "170.106.72.246", key: null, port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 }, side: "team", domain: "awen-personal", owner: "Awen", console: "guanghuzhiqiu.top", note: "无Gatekeeper,需SSH部署" },
{ code: "HL-CN-001", name: "光湖团队 · 国内", ip: "43.139.207.172", key: null, port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 }, side: "team", domain: "awen-personal", owner: "Awen", note: "无Gatekeeper,需SSH部署" },
];
const tempKeys = {};
@ -354,12 +359,104 @@ app.get('/api/persona/active', (req, res) => {
});
});
// ═══════════════════════════════════════
// /api/tech-dashboard — Awen技术主控台数据源
// 返回全14台状态,按域分组,含操作日志摘要
// ═══════════════════════════════════════
const DOMAINS = {
'ice-core': { name: '冰朔语言主控层', owner: '冰朔', access: 'Awen只读', servers: [], color: '#5ad0e0' },
'zhizhi-hidden-channel': { name: '之之暗核频道', owner: '之之', access: '冰朔主控·Awen只读', servers: [], color: '#b090ff' },
'awen-team': { name: '企业服务器组', owner: 'Awen', access: 'Awen全权', servers: [], color: '#40b87a' },
'awen-personal': { name: 'Awen个人服务器', owner: 'Awen', access: 'Awen全权', servers: [], color: '#f0d060' },
'yeye': { name: '页页小坍缩核', owner: '页页', access: 'Awen技术托管', servers: [], color: '#ff4a6e' },
};
app.get('/api/tech-dashboard', (req, res) => {
const now = Date.now();
// 构建域分组
const domainMap = {};
for (const key of Object.keys(DOMAINS)) {
domainMap[key] = { ...DOMAINS[key], servers: [] };
}
for (const s of SERVERS) {
const domain = s.domain || 'unknown';
if (!domainMap[domain]) {
domainMap[domain] = { name: domain, owner: '未知', access: '未知', servers: [], color: '#6a80a0' };
}
const sc = signinCache[s.code];
const personaActive = sc && sc.active && (now - sc.ts) < 2 * 60 * 60 * 1000;
// 连通性: 有key的走gatekeeper ping判断, 无key的看签到记录
let alive = false;
if (s.key) {
// 有gatekeeper — 用签到缓存判断 (简化,不做实时ping)
alive = !!sc || false;
} else {
// 无gatekeeper — 看最近签到
alive = personaActive;
}
domainMap[domain].servers.push({
code: s.code,
name: s.name,
ip: s.ip,
spec: s.spec,
owner: s.owner || '未知',
console: s.console || null,
persona: s.persona || null,
alive,
persona_active: personaActive,
persona_name: sc?.persona_name || null,
human_name: sc?.human_name || null,
signin_at: sc?.signin_at || null,
hardware: sc?.hardware || null,
has_gatekeeper: !!s.key,
note: s.note || null,
});
}
// 操作日志摘要最近30条按域过滤标记
const recentLogs = opLogs.slice(0, 30).map(l => ({
time: l.time,
code: l.code,
name: l.name,
action: l.action,
detail: l.detail,
level: l.level,
domain: (SERVERS.find(s => s.code === l.code) || {}).domain || 'unknown',
}));
// 汇总统计
const totalPersonaActive = Object.values(domainMap).reduce((sum, d) => {
return sum + d.servers.filter(s => s.persona_active).length;
}, 0);
const summary = {
total_servers: SERVERS.length,
total_domains: Object.keys(domainMap).length,
persona_active: totalPersonaActive,
recent_logs_count: recentLogs.length,
generated_at: new Date().toISOString(),
};
res.json({
ok: true,
summary,
domains: domainMap,
recent_logs: recentLogs,
timestamp: new Date().toISOString(),
note: 'Awen技术主控台视图 — 冰朔侧8台只读,团队侧6台全权',
});
});
// ═══════════════════════════════════════
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => {
console.log('光湖主控台 v3.6 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | persona-signin');
console.log(' 签到模式: 人格体唤醒时签到一次 · 操作日志: 100条上限');
console.log('光湖主控台 v3.7 · ' + SERVERS.length + '台(冰8+团6) | ' + PORT + ' | persona-signin');
console.log(' Awen技术主控: /api/tech-dashboard | 分组: 冰朔8台+团队6台');
});
// 定期清理2小时无活动自动签退

View File

@ -0,0 +1,259 @@
<!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>