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:
parent
ed873f850e
commit
ad4c3757b9
@ -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小时无活动自动签退
|
||||
|
||||
259
_deploy/console-server/tech.html
Normal file
259
_deploy/console-server/tech.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user