135 lines
90 KiB
HTML
135 lines
90 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>PIGWay Web Manager</title>
|
||
<style>
|
||
:root{color-scheme:dark;--bg:#0b1118;--panel:#121b25;--panel2:#0f1822;--line:#263546;--text:#e7edf5;--muted:#91a4b8;--good:#50d890;--bad:#ff6b6b;--blue:#58a6ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top right,#14263b 0,#0b1118 42%);color:var(--text);font:14px/1.45 system-ui,-apple-system,sans-serif}main{max-width:1180px;margin:auto;padding:28px 20px 70px}header{display:flex;justify-content:space-between;align-items:end;gap:18px;margin-bottom:22px}h1{font-size:27px;margin:0}h2{font-size:18px;margin:0 0 8px}h3{font-size:15px;margin:0}.sub,.muted,.hint{color:var(--muted)}.hint{font-size:12px;margin-top:4px}.live{display:flex;align-items:center;gap:8px}.dot{width:9px;height:9px;border-radius:50%;background:var(--bad)}.dot.ok{background:var(--good);box-shadow:0 0 12px var(--good)}nav{display:flex;gap:8px;margin:18px 0;position:sticky;top:0;z-index:5;padding:8px 0;background:#0b1118e8;backdrop-filter:blur(8px)}button{border:1px solid var(--line);background:#162230;color:var(--text);border-radius:8px;padding:9px 13px;cursor:pointer}button:hover,button.active{border-color:var(--blue);background:#1b3047}button:disabled{opacity:.45;cursor:not-allowed}.danger{color:#ff8e8e}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.card,.panel,.group{background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:12px;padding:16px;box-shadow:none}.group{background:var(--panel2);margin-top:12px}.group-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.value{font-size:23px;font-weight:700;margin-top:4px}.wide{grid-column:span 2}.panel{margin-top:12px}.rows{display:grid;gap:8px}.row{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:8px 0;border-bottom:1px solid #203040}.row:last-child{border:0}.row-actions{display:flex;gap:6px;align-items:center;flex-wrap:wrap;justify-content:flex-end}.badge{border-radius:999px;padding:3px 8px;background:#243447;white-space:nowrap}.badge.good{color:var(--good)}.badge.bad{color:var(--bad)}.hidden{display:none!important}.log{font:12px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;padding:8px 0;border-bottom:1px solid #203040}.log-label{font:12px system-ui;color:var(--blue);margin-right:8px}.config-shell{display:grid;grid-template-columns:210px minmax(0,1fr);gap:14px;margin-top:14px}.config-menu{display:flex;flex-direction:column;gap:7px;align-self:start;position:sticky;top:66px}.config-menu button{text-align:left}.config-page{display:none}.config-page.active{display:block}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.field label{display:block;color:var(--muted);font-size:12px}.field input,.field select,.service-form input,.service-search{width:100%;margin-top:5px;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:9px}.field input:focus,.field select:focus,.service-form input:focus,.service-search:focus{outline:1px solid var(--blue);border-color:var(--blue)}.input-unit{position:relative}.input-unit input{padding-right:62px}.unit{position:absolute;right:10px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:12px;pointer-events:none}.span2{grid-column:span 2}.rgb-row{display:grid;grid-template-columns:78px repeat(3,minmax(0,1fr));gap:9px;align-items:start}.color-field{display:flex;flex-direction:column;align-items:center}.color-field label{display:block;width:100%;color:var(--muted);font-size:12px;text-align:center}.swatch-wrap{position:relative;width:42px;height:42px;margin:5px auto 0}.swatch{display:block;width:42px;height:42px;border-radius:50%;border:2px solid #fff5;box-shadow:0 0 12px #0008;cursor:pointer}.color-picker{position:absolute;inset:0;width:42px!important;height:42px!important;padding:0!important;margin:0!important;opacity:0;cursor:pointer}.toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:12px 0}.toolbar input{min-width:280px;flex:1;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:9px}.savebar{position:sticky;bottom:10px;z-index:4;background:#101b27ef;border:1px solid var(--line);border-radius:10px;padding:10px;margin-top:18px;box-shadow:0 8px 30px #0008}.service-list{max-height:390px;overflow:auto;border-top:1px solid var(--line);margin-top:10px}.service-form{display:grid;grid-template-columns:1fr 1.4fr;gap:10px}.service-form .full{grid-column:span 2}.service-form label{color:var(--muted);font-size:12px}.error{color:var(--bad)}.success{color:var(--good)}@media(max-width:800px){.grid{grid-template-columns:repeat(2,1fr)}.fields,.service-form{grid-template-columns:1fr}.span2,.service-form .full{grid-column:span 1}.wide{grid-column:span 2}.config-shell{grid-template-columns:1fr}.config-menu{position:static;display:grid;grid-template-columns:repeat(2,1fr)}.rgb-row{grid-template-columns:68px 1fr}.rgb-row .field{grid-column:2}.color-field{grid-row:1/span 3}}@media(max-width:470px){.grid{grid-template-columns:1fr}.wide{grid-column:span 1}header{align-items:start;flex-direction:column}.config-menu{grid-template-columns:1fr}.row{align-items:flex-start;flex-direction:column}.row-actions{justify-content:flex-start}}
|
||
.plugin-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:16px}
|
||
#mainSavebar{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||
</style>
|
||
<style>
|
||
.log-filter-top{display:grid;grid-template-columns:180px 180px auto minmax(190px,1fr);gap:10px;align-items:end;margin:12px 0 10px}.log-filter-bottom{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:10px;align-items:end;margin-bottom:12px}.log-filter-top label,.log-filter-bottom label{color:var(--muted);font-size:12px}.log-filter-top input,.log-filter-bottom select{width:100%;margin-top:5px;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:9px}.quick-range{display:flex;gap:7px;flex-wrap:nowrap;align-items:center}.log-actions{display:flex;gap:8px;align-items:center;justify-content:flex-end;white-space:nowrap}.log-actions #logSummary{margin-right:4px}.log-table-wrap{overflow:auto;border:1px solid var(--line);border-radius:9px}.log-table{width:100%;border-collapse:collapse;min-width:760px}.log-table th,.log-table td{text-align:left;padding:10px;border-bottom:1px solid #203040;vertical-align:top}.log-table th{position:sticky;top:0;background:#152130;z-index:1}.sort-button{border:0;background:transparent;padding:0;font-weight:700}.sort-button:hover{background:transparent}.log-message{font:12px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;word-break:break-word}.log-pager{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:12px}@media(max-width:980px){.log-filter-top{grid-template-columns:1fr 1fr}.quick-range{grid-column:1/span 2;grid-row:2}.log-filter-top>label:last-child{grid-column:1/span 2}.log-filter-bottom{grid-template-columns:repeat(3,1fr)}.log-actions{grid-column:1/span 3}}@media(max-width:600px){.log-filter-top,.log-filter-bottom{grid-template-columns:1fr}.quick-range,.log-filter-top>label:last-child,.log-actions{grid-column:1}.quick-range{overflow-x:auto;justify-content:flex-start}.log-actions{flex-wrap:wrap}.log-actions #logSummary{width:100%;margin:0}}
|
||
.header-actions{display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:flex-end}.preference{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.preference select{height:35px;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:7px 30px 7px 10px}.theme-preference select{min-width:138px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.header-actions .live{height:35px}.service-list{scrollbar-width:none}.service-list:hover{scrollbar-width:thin;scrollbar-color:var(--line) transparent}.service-list::-webkit-scrollbar{width:0}.service-list:hover::-webkit-scrollbar{width:7px}.service-list::-webkit-scrollbar-track{background:transparent}.service-list::-webkit-scrollbar-thumb{background:var(--line);border-radius:999px}[data-theme="light"]{color-scheme:light;--bg:#eef3f8;--panel:#fff;--panel2:#f6f9fc;--line:#c8d4e0;--text:#172333;--muted:#5d7085;--good:#168653;--bad:#c93d3d;--blue:#176fc1}[data-theme="light"] body{background:radial-gradient(circle at top right,#d9e9f8 0,#eef3f8 45%)}[data-theme="light"] nav{background:#eef3f8e8}[data-theme="light"] button{background:#e7eef6}[data-theme="light"] button:hover,[data-theme="light"] button.active{background:#d9e9f8}[data-theme="light"] .field input,[data-theme="light"] .field select,[data-theme="light"] .service-form input,[data-theme="light"] .service-search,[data-theme="light"] .toolbar input,[data-theme="light"] .log-filter-top input,[data-theme="light"] .log-filter-bottom select,[data-theme="light"] .preference select{background:#fff}[data-theme="light"] .badge{background:#e5edf5;color:#31465c}[data-theme="light"] .badge.good{background:#d9f3e5;color:#087444}[data-theme="light"] .badge.bad{background:#fde5e5;color:#b72f2f}[data-theme="light"] .savebar{background:#f7fafcef;box-shadow:0 8px 25px #3453}[data-theme="light"] .log-table th{background:#e7eef6}[data-theme="light"] .row,[data-theme="light"] .log-table th,[data-theme="light"] .log-table td,[data-theme="light"] .log,[data-theme="light"] .service-list{border-color:#d6e0ea}@media(max-width:470px){.header-actions{justify-content:flex-start}}.host-panel{margin-top:14px;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease}.host-head{display:flex;justify-content:space-between;gap:16px;align-items:start;margin-bottom:12px}.device-details{display:flex;gap:8px 16px;flex-wrap:wrap;font-size:12px;color:var(--muted);margin-top:5px}.host-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.metric{background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px}.metric .value{font-size:20px}.progress{height:7px;background:color-mix(in srgb,var(--line) 70%,transparent);border-radius:999px;overflow:hidden;margin:8px 0}.progress span{display:block;height:100%;background:var(--blue);border-radius:inherit}.host-select{display:grid;gap:4px;color:var(--muted);font-size:12px}.host-select select{min-width:210px;border:1px solid var(--line);border-radius:7px;background:var(--panel2);color:var(--text);padding:9px}.config-target-toolbar{display:grid;grid-template-columns:minmax(220px,.65fr) minmax(0,1.35fr);align-items:end}.token-control{display:flex;gap:8px;align-items:center}.token-control input{min-width:0;margin:0}.token-control button{white-space:nowrap}.bulk-tools{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}.bulk-section{padding:12px}.bulk-section-title{display:flex;align-items:center;gap:8px;font-weight:700}.bulk-items{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 18px;margin:10px 0 0 24px}.bulk-item{display:flex;align-items:flex-start;gap:7px}.bulk-item span{min-width:0}.bulk-value{display:block;color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bulk-target{display:grid;grid-template-columns:auto minmax(120px,.5fr) minmax(220px,1fr);gap:10px;align-items:center}.bulk-target input[type="password"],.bulk-target input[type="text"]{width:100%;border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:8px}.host-checks{display:flex;gap:12px;flex-wrap:wrap}.host-checks label{display:flex;gap:6px;align-items:center}.offline-card{border-color:var(--bad)}@media(hover:hover) and (pointer:fine){.host-panel:hover{transform:translateY(-3px);border-color:color-mix(in srgb,var(--blue) 45%,var(--line));box-shadow:0 16px 34px #0006}[data-theme="light"] .host-panel:hover{box-shadow:0 14px 30px #3454}}@media(max-width:900px){.host-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:650px){.config-target-toolbar{grid-template-columns:1fr}.bulk-items{grid-template-columns:1fr}.bulk-target{grid-template-columns:auto 1fr}.bulk-target input{grid-column:1/span 2}}@media(max-width:550px){.host-grid{grid-template-columns:1fr}.host-head{flex-direction:column}}
|
||
.plugin-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:16px}
|
||
#mainSavebar{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||
.action-row,.savebar,.plugin-actions{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.action-row>button{flex-shrink:0}.bulk-tools{column-gap:16px}.group-head{column-gap:16px}
|
||
.language-preference{position:relative}.language-preference .language-icon{position:absolute;left:10px;width:16px;height:16px;pointer-events:none;color:var(--text)}.language-preference select{padding-left:34px;min-width:110px}
|
||
.theme-preference{position:relative}.theme-preference .preference-icon{position:absolute;left:10px;width:16px;height:16px;display:flex;align-items:center;justify-content:center;pointer-events:none;color:var(--text);font-size:16px;line-height:1}.theme-preference select{padding-left:34px}
|
||
</style>
|
||
</head>
|
||
<body><main>
|
||
<header><div><h1>PIGWay Web Manager</h1><div class="sub" id="identity">正在连接管理中心…</div></div><div class="header-actions"><label class="preference language-preference"><span class="sr-only">Language / 语言</span><svg class="language-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18M5 6h14M5 18h14"/></svg><select id="language" aria-label="Language / 语言" title="Language / 语言"><option value="zh">中文</option><option value="en">English</option></select></label><label class="preference theme-preference"><span class="sr-only">颜色模式</span><span id="themeIcon" class="preference-icon" aria-hidden="true">◐</span><select id="theme" aria-label="颜色模式"><option value="auto">跟随系统</option><option value="light">浅色模式</option><option value="dark">深色模式</option></select></label><div class="live"><span class="dot" id="dot"></span><span id="liveText">离线</span></div></div></header>
|
||
<section id="managerLogin" class="panel"><label class="field"><span>Manager Token / 管理中心令牌</span><input id="managerToken" type="password" autocomplete="current-password"></label><div class="action-row" style="margin-top:16px"><button id="managerConnect">Connect / 连接</button><span id="managerLoginState"></span></div></section><nav><button class="active" data-tab="overview">运行状态</button><button data-tab="logs">事件日志</button><button data-tab="machines">机器管理</button><button data-tab="config">系统配置</button></nav>
|
||
<section id="overview"><div id="hostCards"><div class="panel muted">正在读取主机状态…</div></div></section>
|
||
<section id="logs" class="hidden"><div class="panel"><h2>事件日志</h2><p class="muted">查询 systemd journal 中仍在保留的全部硬件 Agent 事件。</p><div class="log-filter-top"><label>开始日期<input id="logStart" type="date"></label><label>结束日期<input id="logEnd" type="date"></label><div class="quick-range"><button data-log-range="today">今天</button><button data-log-range="yesterday">昨天</button><button data-log-range="week">本周</button><button data-log-range="month">本月</button><button data-log-range="all">全部</button></div><label>关键词<input id="logSearch" placeholder="搜索日志内容"></label></div><div class="log-filter-bottom"><label>严重程度<select id="logSeverity"><option value="">全部程度</option><option value="DEBUG">调试</option><option value="INFO">信息</option><option value="NOTICE">注意</option><option value="WARN">警告</option><option value="ERROR">错误</option><option value="CRITICAL">严重</option><option value="ALERT,EMERGENCY">紧急</option></select></label><label>机器标识<select id="logMachine"><option value="">全部机器</option></select></label><label>事件类型<select id="logEvent"><option value="">全部事件</option></select></label><div class="log-actions"><span class="muted" id="logSummary"></span><button id="refreshLogs">查询</button><button id="resetLogs">重置</button></div></div><div class="log-table-wrap"><table class="log-table"><thead><tr><th><button class="sort-button" data-log-sort="timestamp">时间</button></th><th><button class="sort-button" data-log-sort="machine">机器</button></th><th><button class="sort-button" data-log-sort="severity">程度</button></th><th><button class="sort-button" data-log-sort="event">事件类型</button></th><th><button class="sort-button" data-log-sort="message">内容</button></th></tr></thead><tbody id="logRows"></tbody></table></div><div class="log-pager"><button id="logPrev">上一页</button><span class="muted" id="logPage"></span><button id="logNext">下一页</button></div></div></section>
|
||
<section id="machines" class="hidden"><div class="panel"><h2>机器管理</h2><div id="machineContent"></div></div></section>
|
||
<section id="config" class="hidden"><div class="panel"><h2>系统配置</h2><p class="muted">配置按用途分组。监控设置与硬件插件配置独立保存。</p><div class="toolbar config-target-toolbar"><label class="host-select">配置机器<select id="configHost"></select></label></div><div class="config-shell"><aside class="config-menu" id="configMenu"></aside><div id="configRows"></div></div><div class="savebar" id="mainSavebar"><button id="saveConfig">保存到当前机器</button> <span id="saveState" class="muted">尚未修改</span></div></div></section>
|
||
</main><script>
|
||
let LANG=localStorage.getItem('pigwayLanguage')||'zh';
|
||
const EN={"硬件插件":"Hardware plugins","独立安装,按实际能力提供风扇、屏幕或灯光控制":"Install independently; fan, display and lighting controls follow advertised capabilities","配置按用途分组。监控设置与硬件插件配置独立保存。":"Settings are grouped by purpose. Monitoring and hardware plugin settings are saved independently.","未启用":"Disabled","硬件插件已启用":"Hardware plugin enabled","硬件插件已关闭":"Hardware plugin disabled","硬件插件不可用":"Hardware plugin unavailable","硬件插件": "Hardware plugin", "可选 Yahboom 散热板;未安装时保持关闭": "Optional Yahboom cooling HAT; leave disabled when absent", "启用散热板(OLED、风扇、RGB)": "Enable cooling HAT (OLED, fan, RGB)", "告警灯效模式": "Alert lighting mode", "自定义闪烁最小保持时间": "Minimum custom flash hold", "默认关闭;启用前运行安装器 --enable-cooling-hat 安装依赖。": "Disabled by default; install dependencies with --enable-cooling-hat before enabling.", "推荐内置呼吸;自定义闪烁仍可能异色或延迟。": "Built-in breathing recommended; custom flashing may still show wrong colors or delays.", "范围 1500–60000 毫秒。实际亮灭时间不会低于此值。": "Range 1500–60000 ms. Each light/dark state lasts at least this long.", "内置呼吸(推荐)": "Built-in breathing (recommended)", "自定义颜色闪烁(实验性)": "Custom color flashing (experimental)", "散热板插件关闭时,灯光与风扇设置不控制硬件。": "Lighting and fan settings do not control hardware while the plugin is disabled.", "内置呼吸:CPU 红、电源黄、内存紫、存储白、网络蓝、服务青。": "Built-in breathing: CPU red, power yellow, memory purple, storage white, network blue, services cyan.", "普通、严重、紧急对应 MCU 速度档 1、2、3;不支持自定义呼吸颜色或精确周期。": "Warning, critical and emergency use MCU speed levels 1, 2 and 3; custom breathing colors and exact periods are unsupported.", "启用": "Enabled",
|
||
'语言':'Language','颜色模式':'Appearance','中文':'中文','自动':'Auto','深色':'Dark','浅色':'Light','◐ 跟随系统':'◐ Follow system','☀ 浅色模式':'☀ Light mode','☾ 深色模式':'☾ Dark mode','正在连接管理中心…':'Connecting to Raspberry Pi…','在线':'Online','离线':'Offline',
|
||
'运行状态':'Status','事件日志':'Event log','系统配置':'Settings','处理器负载':'CPU load','处理器温度':'CPU temperature','内存占用':'Memory usage','磁盘占用':'Disk usage','主网络':'Primary network','显示状态':'Display status','当前告警':'Active alerts','无告警':'No alerts','服务与进程监控':'Service and process monitors',
|
||
'查询 systemd journal 中仍在保留的全部硬件 Agent 事件。':'Query all retained hardware Agent events in the systemd journal.','今天':'Today','昨天':'Yesterday','本周':'This week','本月':'This month','全部':'All','开始日期':'Start date','结束日期':'End date','严重程度':'Severity','全部程度':'All severities','调试':'Debug','信息':'Info','注意':'Notice','警告':'Warning','错误':'Error','严重':'Critical','紧急':'Emergency','事件类型':'Event type','全部事件':'All events','风扇档位变化':'Fan level changed','关键词':'Keyword','搜索日志内容':'Search log messages','查询':'Search','重置':'Reset','时间':'Time','程度':'Severity','内容':'Message','上一页':'Previous','下一页':'Next',
|
||
'配置按用途分组。监控设置与硬件插件配置独立保存。':'Settings are grouped by purpose and only relevant options are shown. Saving creates a backup and restarts the hardware Agent.','配置写入令牌(Bearer Token)':'Configuration Bearer Token','显示令牌':'Show token','隐藏令牌':'Hide token','保存并应用':'Save and apply','尚未修改':'No changes',
|
||
'灯光设置':'Lighting','正常灯效与告警颜色':'Normal effects and alert colors','告警阈值':'Alert thresholds','处理器、内存、温度、磁盘和 Wi-Fi':'CPU, memory, temperature, disk and Wi-Fi','风扇温控':'Fan control','温度档位与降档回差':'Temperature levels and hysteresis','刷新周期':'Refresh intervals','传感器、屏幕、网络和健康检查':'Sensors, display, network and health checks','屏幕布局':'Display layout','OLED 四行文字位置':'OLED four-line positions','板载指示灯':'Board LEDs','树莓派电源灯与活动灯':'Raspberry Pi power and activity LEDs','服务监控':'Service monitoring','监控 systemd 服务':'Monitor systemd services','进程监控':'Process monitoring','监控普通进程关键字':'Monitor process keywords','API 设置':'Web and API','API 开关、监听地址、端口和日志数量':'Bind address, port and log count',
|
||
'正常运行灯效':'Normal lighting','没有告警时显示;告警出现后自动让位。':'Shown when no alert is active; alerts take priority.','告警对象颜色':'Alert category colors','告警对象决定颜色,严重程度决定闪烁速度。':'The alert category determines color; severity determines flash timing.','普通告警闪烁节奏':'Warning flash timing','严重告警闪烁节奏':'Critical flash timing','紧急告警闪烁节奏':'Emergency flash timing','硬件高级参数':'Advanced hardware settings','自定义颜色':'Custom color','点击色块打开色盘':'Click the swatch to open the color picker','颜色':'Color','红色通道':'Red channel','绿色通道':'Green channel','蓝色通道':'Blue channel','范围 0–255。':'Range: 0–255.','亮灯时间':'On time','灭灯时间':'Off time','毫秒':'ms','秒':'s','像素':'px','条':'entries',
|
||
'当前系统服务':'System services','点击“加入监控”会自动填写下方表单;也可以手动输入尚未安装的未来服务。':'Click “Add monitor” to fill the form below, or enter a future service manually.','刷新列表':'Refresh list','搜索服务名称或说明':'Search service name or description','正在读取 systemd 服务…':'Loading systemd services…','已监控服务':'Monitored services','“暂停监控”只停止状态检查,不影响服务运行;“停止服务”才会操作 systemd。':'“Pause monitor” stops only status checks; “Stop service” operates systemd.','新建服务监控':'Add service monitor','编辑服务监控':'Edit service monitor','OLED 显示名称':'OLED display name','systemd 服务名称':'systemd service name','备注说明':'Description','保存监控':'Save monitor','清空':'Clear','未监控':'Not monitored','已监控':'Monitored','已暂停':'Paused','加入监控':'Add monitor','编辑监控':'Edit monitor','暂停监控':'Pause monitor','恢复监控':'Resume monitor','停止服务':'Stop service','启动服务':'Start service','编辑':'Edit','删除监控':'Delete monitor','正在运行':'Running','未运行':'Stopped','监控已暂停':'Monitor paused','运行中':'Running','服务已停止':'Service stopped','备注:':'Note: ','未填写':'Not provided','暂无服务说明':'No service description','没有匹配的服务':'No matching services','尚未监控任何服务':'No monitored services','正在保存…':'Saving…','保存成功':'Saved','暂无配置项':'No settings','暂无数据':'No data',
|
||
'程序启动':'Agent started','程序停止':'Agent stopped','告警出现':'Alert active','告警恢复':'Alert recovered','显示对象切换':'Display owner changed','互联网断开':'Internet down','互联网恢复':'Internet recovered','网络接口切换':'Network interface changed','RGB 控制错误':'RGB control error','RGB 模式切换':'RGB mode changed','RGB 控制恢复':'RGB control recovered','系统事件':'System event','监控对象停止':'Monitored target down','监控对象恢复':'Monitored target recovered','网络断开':'Network down','网络恢复':'Network recovered','供电欠压':'Power undervoltage','供电恢复':'Power recovered','历史电源状态':'Power history flag','根文件系统只读':'Root filesystem read-only',
|
||
'正常主页':'Normal home','处理器温度严重':'Critical CPU temperature','处理器降频':'CPU throttled','频率受限':'Frequency capped','服务停止':'Service down','进程停止':'Process down','磁盘严重不足':'Disk critically full','处理器温度偏高':'High CPU temperature','处理器负载过高':'High CPU load','内存占用过高':'High memory usage','磁盘空间不足':'Disk usage high','Wi-Fi 信号弱':'Weak Wi-Fi signal','全灭':'Off','普通告警':'Warning','严重告警':'Critical','紧急告警':'Emergency','无线网络':'Wi-Fi','有线网络':'Ethernet','正常':'Normal','停止':'Stopped','系统服务':'System service','普通进程':'Process','当前没有告警':'No active alerts','没有符合条件的日志':'No matching events','数据来源:systemd journal':'Source: systemd journal','项尚未保存':'unsaved changes','没有需要保存的修改':'No changes to save','正在保存并应用…':'Saving and applying…'
|
||
};
|
||
Object.assign(EN,{
|
||
'正常状态灯效':'Normal mode','自定义颜色常亮':'Custom solid color','自定义颜色闪烁':'Custom color flash','流水灯':'Flow','呼吸灯':'Breathe','跑马灯':'Marquee','彩虹灯':'Rainbow','炫彩灯':'Colorful','内置效果速度':'Built-in effect speed','内置效果颜色':'Built-in effect color','慢速':'Slow','中速':'Medium','快速':'Fast','红色':'Red','绿色':'Green','蓝色':'Blue','黄色':'Yellow','紫色':'Purple','青色':'Cyan','白色':'White','关闭':'Off','不干预':'Unmanaged',
|
||
'普通告警亮灯':'Warning on time','普通告警灭灯':'Warning off time','严重告警亮灯':'Critical on time','严重告警灭灯':'Critical off time','紧急告警亮灯':'Emergency on time','紧急告警灭灯':'Emergency off time','RGB 寄存器写入间隔':'RGB register write interval','处理器高负载阈值':'High CPU threshold','处理器恢复阈值':'CPU recovery threshold','处理器持续时间':'CPU duration','内存高占用阈值':'High memory threshold','内存恢复阈值':'Memory recovery threshold','内存持续时间':'Memory duration','高温告警阈值':'High temperature threshold','严重高温阈值':'Critical temperature threshold','磁盘高占用阈值':'High disk threshold','磁盘严重阈值':'Critical disk threshold','Wi-Fi 弱信号阈值':'Weak Wi-Fi threshold','Wi-Fi 恢复阈值':'Wi-Fi recovery threshold','弱信号持续时间':'Weak signal duration',
|
||
'风扇启动温度':'Fan start temperature','二档起始温度':'Level 2 temperature','三档起始温度':'Level 3 temperature','四档起始温度':'Level 4 temperature','全速起始温度':'Full-speed temperature','降档温度回差':'Downshift hysteresis','传感器采样周期':'Sensor interval','OLED 刷新周期':'OLED refresh interval','OLED 页面轮播周期':'OLED page interval','网络检查周期':'Network check interval','服务检查周期':'Service check interval','健康检查周期':'Health check interval','第一行纵坐标':'Row 1 Y position','第二行纵坐标':'Row 2 Y position','第三行纵坐标':'Row 3 Y position','第四行纵坐标':'Row 4 Y position','树莓派电源灯':'Raspberry Pi power LED','树莓派活动灯':'Raspberry Pi activity LED','监听地址':'Bind address','监听端口':'Listen port','默认日志条数':'Default log entries',
|
||
'处理器、温度与散热':'CPU, temperature and cooling','供电与欠压':'Power and undervoltage','内存':'Memory','存储':'Storage','网络与 Wi-Fi':'Network and Wi-Fi','服务与进程':'Services and processes','仅在没有当前告警时生效;告警会立即抢占。':'Only active when there are no alerts; alerts take priority immediately.','MCU 内置速度档位。':'MCU built-in speed level.','仅流水灯和呼吸灯使用。':'Used only by flow and breathe modes.','MCU 内置灯效参数写入间隔,通常保持 10 毫秒。':'MCU built-in effect parameter write interval; normally keep 10 ms.','单位 dBm,数值越接近 0 信号越强。':'Unit: dBm. Values closer to 0 mean a stronger signal.','单位 dBm,应高于弱信号阈值。':'Unit: dBm. Must be above the weak-signal threshold.','避免临界温度附近频繁切换档位。':'Prevents rapid level changes near a threshold.','选择“关闭”时由程序熄灭板载灯。':'Choose “Off” to let the program turn off the board LED.'
|
||
});
|
||
Object.assign(EN,{'机器标识':'Host ID','机器':'Host','全部机器':'All hosts','配置机器':'Configure host','多机配置同步':'Multi-host configuration sync','勾选目标机器后,将当前页面中已修改的配置项批量同步到这些机器。':'Select target hosts to copy the changed settings on this page.','当前机器配置令牌(Bearer Token)':'Current host Bearer Token','保存到当前机器':'Save to current host','批量同步已修改项':'Sync changed settings','设备标识':'Device identity','自定义本机显示名称和稳定标识':'Set the local display name and stable identifier','显示名称':'Display name','机器列表':'Host list','登记需要聚合监控的其他节点':'Register nodes for fleet monitoring','已登记机器':'Registered hosts','尚未登记其他机器':'No remote hosts registered','添加或更新机器':'Add or update host','节点地址':'Node URL','保存机器':'Save host','正在读取主机状态…':'Loading host status…','可用':'Available','剩余':'Free','本机':'Local','连接失败':'Connection failed'});
|
||
Object.assign(EN,{'端口':'port','0.0.0.0 允许局域网访问;127.0.0.1 仅本机。':'0.0.0.0 allows LAN access; 127.0.0.1 is local only.','范围 1–65535;保存后 API 自动重启。':'Range: 1–65535. The API restarts after saving.','API 服务不能停止自身':'The API service cannot stop itself','已自动填写,可继续修改。':'Filled automatically; you can continue editing.','请先选择同步目标机器':'Select target hosts first'});
|
||
Object.assign(EN,{'批量同步':'Batch sync','从当前配置机器复制所选配置项到目标机器':'Copy selected settings from the current configuration host to target hosts','来源机器':'Source host','目标机器':'Target hosts','配置项':'Settings','全部选择':'Select all','全部取消':'Clear all','反选':'Invert selection','开始批量同步':'Start batch sync','机器标识和机器列表不会参与同步。':'Host identity and host registry are never synchronized.','请选择至少一个配置项':'Select at least one setting','请选择至少一台目标机器':'Select at least one target host','不能选择来源机器作为目标':'The source host cannot also be a target','目标机器令牌':'Target host token','项已选择':'settings selected'});
|
||
Object.assign(EN,{'硬件插件':'Hardware plugins','配置按用途分组。监控设置与硬件插件配置独立保存。':'Monitoring settings and hardware plugin settings are saved independently.'});
|
||
Object.assign(EN,{'机器管理':'Machine management','恢复设备默认值':'Restore device defaults','本机':'Local host'});
|
||
Object.assign(EN,{'API 设置':'API settings','API 开关、监听地址、端口和日志数量':'API listener, port and log settings','启用远程管理 API':'Enable remote management API','开启':'Enabled','正在连接管理中心…':'Connecting to manager…'});
|
||
const ZH=Object.fromEntries(Object.entries(EN).map(([zh,en])=>[en,zh])),tr=s=>LANG==='en'?(EN[s]||s):(ZH[s]||s);
|
||
function translateNode(root=document){const map=LANG==='en'?EN:ZH,translate=value=>{const trim=value.trim();return trim&&map[trim]?value.replace(trim,map[trim]):value};const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);let n;while(n=walker.nextNode())n.nodeValue=translate(n.nodeValue);root.querySelectorAll?.('[placeholder],[title],[aria-label]').forEach(e=>['placeholder','title','aria-label'].forEach(a=>{const v=e.getAttribute(a);if(v)e.setAttribute(a,translate(v))}))}
|
||
function applyTheme(value){$('themeIcon').textContent=({auto:"◐",light:"☀",dark:"☾"})[value]||"◐";const resolved=value==='auto'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):value;document.documentElement.dataset.theme=resolved}
|
||
const $=id=>document.getElementById(id),tabs=[...document.querySelectorAll('nav button')];let original={},fleetHosts=[],currentConfigHost='';const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const sectionMeta={plugins:['硬件插件','独立安装,按实际能力提供风扇、屏幕或灯光控制'],hardware:['硬件插件','可选 Yahboom 散热板;未安装时保持关闭'],device:['设备标识','自定义本机显示名称和稳定标识'],hosts:['机器列表','登记需要聚合监控的其他节点'],led:['灯光设置','正常灯效与告警颜色'],alerts:['告警阈值','处理器、内存、温度、磁盘和 Wi-Fi'],fan:['风扇温控','温度档位与降档回差'],timing:['刷新周期','传感器、屏幕、网络和健康检查'],display:['屏幕布局','OLED 四行文字位置'],dark_mode:['板载指示灯','树莓派电源灯与活动灯'],services:['服务监控','监控 systemd 服务'],processes:['进程监控','监控普通进程关键字'],api:['API 设置','API 开关、监听地址、端口和日志数量'],bulk_sync:['批量同步','从当前配置机器复制所选配置项到目标机器']};
|
||
const labels={'api.enabled':'启用远程管理 API','hardware.cooling_hat_enabled':'启用散热板(OLED、风扇、RGB)','led.alert_mode':'告警灯效模式','led.custom_min_hold_ms':'自定义闪烁最小保持时间','led.normal_mode':'正常状态灯效','led.normal_r':'红色通道','led.normal_g':'绿色通道','led.normal_b':'蓝色通道','led.normal_flash_on_ms':'亮灯时间','led.normal_flash_off_ms':'灭灯时间','led.normal_effect_speed':'内置效果速度','led.normal_effect_color':'内置效果颜色','led.warning_flash_on_ms':'普通告警亮灯','led.warning_flash_off_ms':'普通告警灭灯','led.critical_flash_on_ms':'严重告警亮灯','led.critical_flash_off_ms':'严重告警灭灯','led.emergency_flash_on_ms':'紧急告警亮灯','led.emergency_flash_off_ms':'紧急告警灭灯','led.write_delay_ms':'RGB 寄存器写入间隔','alerts.cpu_high':'处理器高负载阈值','alerts.cpu_recover':'处理器恢复阈值','alerts.cpu_high_seconds':'处理器持续时间','alerts.memory_high':'内存高占用阈值','alerts.memory_recover':'内存恢复阈值','alerts.memory_high_seconds':'内存持续时间','alerts.temp_high':'高温告警阈值','alerts.temp_critical':'严重高温阈值','alerts.disk_high':'磁盘高占用阈值','alerts.disk_critical':'磁盘严重阈值','alerts.wifi_weak':'Wi-Fi 弱信号阈值','alerts.wifi_recover':'Wi-Fi 恢复阈值','alerts.wifi_weak_seconds':'弱信号持续时间','fan.start':'风扇启动温度','fan.level2':'二档起始温度','fan.level3':'三档起始温度','fan.level4':'四档起始温度','fan.max':'全速起始温度','fan.hysteresis':'降档温度回差','timing.sensor_interval':'传感器采样周期','timing.oled_refresh_interval':'OLED 刷新周期','timing.page_interval':'OLED 页面轮播周期','timing.network_interval':'网络检查周期','timing.service_interval':'服务检查周期','timing.health_interval':'健康检查周期','display.row1_y':'第一行纵坐标','display.row2_y':'第二行纵坐标','display.row3_y':'第三行纵坐标','display.row4_y':'第四行纵坐标','dark_mode.pi_pwr_led':'树莓派电源灯','dark_mode.pi_act_led':'树莓派活动灯','api.bind':'监听地址','api.port':'监听端口','api.log_limit':'默认日志条数','device.identifier':'机器标识','device.name':'显示名称'};
|
||
['cpu','power','memory','storage','network','service'].forEach(g=>['r','g','b'].forEach(c=>labels[`led.${g}_${c}`]=`${({r:'红',g:'绿',b:'蓝'})[c]}色通道`));
|
||
const hints={'hardware.cooling_hat_enabled':'默认关闭;启用前运行安装器 --enable-cooling-hat 安装依赖。','led.alert_mode':'推荐内置呼吸;自定义闪烁仍可能异色或延迟。','led.custom_min_hold_ms':'范围 1500–60000 毫秒。实际亮灭时间不会低于此值。','led.normal_mode':'仅在没有当前告警时生效;告警会立即抢占。','led.normal_r':'范围 0–255。','led.normal_g':'范围 0–255。','led.normal_b':'范围 0–255。','led.normal_flash_on_ms':'范围 50–60000 毫秒。','led.normal_flash_off_ms':'范围 50–60000 毫秒。','led.normal_effect_speed':'MCU 内置速度档位。','led.normal_effect_color':'仅流水灯和呼吸灯使用。','led.write_delay_ms':'MCU 内置灯效参数写入间隔,通常保持 10 毫秒。','alerts.wifi_weak':'单位 dBm,数值越接近 0 信号越强。','alerts.wifi_recover':'单位 dBm,应高于弱信号阈值。','fan.hysteresis':'避免临界温度附近频繁切换档位。','api.bind':'0.0.0.0 允许局域网访问;127.0.0.1 仅本机。','api.port':'范围 1–65535;保存后 API 自动重启。','dark_mode.pi_pwr_led':'选择“关闭”时由程序熄灭板载灯。','dark_mode.pi_act_led':'选择“关闭”时由程序熄灭板载灯。'};
|
||
['normal','cpu','power','memory','storage','network','service'].forEach(g=>['r','g','b'].forEach(c=>hints[`led.${g}_${c}`]='范围 0–255。'));
|
||
const options={'api.enabled':[['false','关闭'],['true','开启']],'hardware.cooling_hat_enabled':[['false','关闭'],['true','启用']],'led.alert_mode':[['breathe','内置呼吸(推荐)'],['flash','自定义颜色闪烁(实验性)']],'led.normal_mode':[['off','全灭'],['solid','自定义颜色常亮'],['flash','自定义颜色闪烁'],['flow','流水灯'],['breathe','呼吸灯'],['marquee','跑马灯'],['rainbow','彩虹灯'],['colorful','炫彩灯']],'led.normal_effect_speed':[['1','慢速'],['2','中速'],['3','快速']],'led.normal_effect_color':[['red','红色'],['green','绿色'],['blue','蓝色'],['yellow','黄色'],['purple','紫色'],['cyan','青色'],['white','白色']],'dark_mode.pi_pwr_led':[['off','关闭'],['on','不干预']],'dark_mode.pi_act_led':[['off','关闭'],['on','不干预']]};
|
||
const numberRules={'led.custom_min_hold_ms':[1500,60000,100],'led.normal_r':[0,255,1],'led.normal_g':[0,255,1],'led.normal_b':[0,255,1],'led.normal_flash_on_ms':[50,60000,10],'led.normal_flash_off_ms':[50,60000,10],'led.warning_flash_on_ms':[50,60000,10],'led.warning_flash_off_ms':[50,60000,10],'led.critical_flash_on_ms':[50,60000,10],'led.critical_flash_off_ms':[50,60000,10],'led.emergency_flash_on_ms':[50,60000,10],'led.emergency_flash_off_ms':[50,60000,10],'led.write_delay_ms':[1,1000,1],'alerts.cpu_high':[0,100,1],'alerts.cpu_recover':[0,100,1],'alerts.cpu_high_seconds':[1,86400,1],'alerts.memory_high':[0,100,1],'alerts.memory_recover':[0,100,1],'alerts.memory_high_seconds':[1,86400,1],'alerts.temp_high':[0,120,.1],'alerts.temp_critical':[0,120,.1],'alerts.disk_high':[0,100,1],'alerts.disk_critical':[0,100,1],'alerts.wifi_weak':[-120,0,1],'alerts.wifi_recover':[-120,0,1],'alerts.wifi_weak_seconds':[1,86400,1],'fan.start':[0,100,.1],'fan.level2':[0,100,.1],'fan.level3':[0,100,.1],'fan.level4':[0,100,.1],'fan.max':[0,100,.1],'fan.hysteresis':[0,20,.1],'timing.sensor_interval':[.2,3600,.1],'timing.oled_refresh_interval':[.2,3600,.1],'timing.page_interval':[1,3600,.1],'timing.network_interval':[1,3600,.1],'timing.service_interval':[1,3600,.1],'timing.health_interval':[.2,3600,.1],'display.row1_y':[0,31,1],'display.row2_y':[0,31,1],'display.row3_y':[0,31,1],'display.row4_y':[0,31,1],'api.port':[1,65535,1],'api.log_limit':[1,1000,1]};['cpu','power','memory','storage','network','service'].forEach(g=>['r','g','b'].forEach(c=>numberRules[`led.${g}_${c}`]=[0,255,1]));
|
||
const units={'led.custom_min_hold_ms':'毫秒','led.normal_flash_on_ms':'毫秒','led.normal_flash_off_ms':'毫秒','led.warning_flash_on_ms':'毫秒','led.warning_flash_off_ms':'毫秒','led.critical_flash_on_ms':'毫秒','led.critical_flash_off_ms':'毫秒','led.emergency_flash_on_ms':'毫秒','led.emergency_flash_off_ms':'毫秒','led.write_delay_ms':'毫秒','alerts.cpu_high':'%','alerts.cpu_recover':'%','alerts.cpu_high_seconds':'秒','alerts.memory_high':'%','alerts.memory_recover':'%','alerts.memory_high_seconds':'秒','alerts.temp_high':'°C','alerts.temp_critical':'°C','alerts.disk_high':'%','alerts.disk_critical':'%','alerts.wifi_weak':'dBm','alerts.wifi_recover':'dBm','alerts.wifi_weak_seconds':'秒','fan.start':'°C','fan.level2':'°C','fan.level3':'°C','fan.level4':'°C','fan.max':'°C','fan.hysteresis':'°C','timing.sensor_interval':'秒','timing.oled_refresh_interval':'秒','timing.page_interval':'秒','timing.network_interval':'秒','timing.service_interval':'秒','timing.health_interval':'秒','display.row1_y':'像素','display.row2_y':'像素','display.row3_y':'像素','display.row4_y':'像素','api.port':'端口','api.log_limit':'条'};
|
||
const sectionOrder=['device','plugins','alerts','timing','dark_mode','services','processes','api'],bulkSections=sectionOrder.filter(s=>!['device','hosts','plugins'].includes(s)),alertColorGroups=[['cpu','处理器、温度与散热'],['power','供电与欠压'],['memory','内存'],['storage','存储'],['network','网络与 Wi-Fi'],['service','服务与进程']];
|
||
const ownerNames={NORMAL_HOME:'正常主页',ROOT_READONLY:'根文件系统只读',TEMP_CRITICAL:'处理器温度严重',POWER_UNDERVOLT:'供电欠压',THROTTLED:'处理器降频',FREQ_CAPPED:'频率受限',NETWORK_DOWN:'网络断开',INTERNET_DOWN:'互联网断开',SERVICE_DOWN:'服务停止',PROCESS_DOWN:'进程停止',DISK_CRITICAL:'磁盘严重不足',TEMP_HIGH:'处理器温度偏高',CPU_HIGH:'处理器负载过高',MEMORY_HIGH:'内存占用过高',DISK_HIGH:'磁盘空间不足',WIFI_WEAK:'Wi-Fi 信号弱'},rgbNames={DISABLED:'未启用',OFF:'全灭',NORMAL_SOLID:'正常 · 自定义常亮',NORMAL_FLASH:'正常 · 自定义闪烁',NORMAL_FLOW:'正常 · 流水灯',NORMAL_BREATHE:'正常 · 呼吸灯',NORMAL_MARQUEE:'正常 · 跑马灯',NORMAL_RAINBOW:'正常 · 彩虹灯',NORMAL_COLORFUL:'正常 · 炫彩灯'},eventNames={HARDWARE_ENABLED:'硬件插件已启用',HARDWARE_DISABLED:'硬件插件已关闭',HARDWARE_UNAVAILABLE:'硬件插件不可用',FAN_LEVEL:'风扇档位变化',START:'程序启动',STOP:'程序停止',ALERT_ACTIVE:'告警出现',ALERT_RECOVERED:'告警恢复',DISPLAY_OWNER:'显示对象切换',INTERNET_DOWN:'互联网断开',INTERNET_RECOVERED:'互联网恢复',NET_SWITCH:'网络接口切换',NETWORK_DOWN:'网络断开',NETWORK_RECOVERED:'网络恢复',POWER_LOW:'供电欠压',POWER_RECOVERED:'供电恢复',POWER_HISTORY:'历史电源状态',HISTORY_FLAG:'历史电源状态',ROOT_READONLY:'根文件系统只读',RGB_ERROR:'RGB 控制错误',RGB_MODE:'RGB 模式切换',RGB_RECOVERED:'RGB 控制恢复',SYSTEM:'系统事件',WATCH_DOWN:'监控对象停止',WATCH_RECOVERED:'监控对象恢复'};
|
||
function ownerName(id){const [type,suffix]=String(id||'').split('|',2),name=ownerNames[type]||type;return tr(name)+(suffix?` · ${suffix}`:'')}function rgbName(mode){if(rgbNames[mode])return tr(rgbNames[mode]);const p=String(mode||'').split('_'),groups={CPU:'处理器',POWER:'供电',MEMORY:'内存',STORAGE:'存储',NETWORK:'网络',SERVICE:'服务'},levels={WARNING:'普通告警',CRITICAL:'严重告警',EMERGENCY:'紧急告警'};return`${tr(groups[p[0]]||p[0])} · ${tr(levels[p[1]]||p[1])}${p[2]==='BREATHE'?(LANG==='zh'?'呼吸':' breathing'):(LANG==='zh'?'闪烁':' flash')}`}function eventName(id){return tr(eventNames[id]||(LANG==='zh'?`未知事件(${id})`:String(id).toLowerCase().replaceAll('_',' ')))}
|
||
function field(section,key,value,extra=''){const id=`${section}.${key}`,opts=options[id],rule=numberRules[id],label=tr(labels[id]||key),help=tr(hints[id]||''),unit=tr(units[id]||'');let control;if(opts)control=`<select data-section="${esc(section)}" data-key="${esc(key)}">${opts.map(([v,l])=>`<option value="${v}" ${String(value)===v?'selected':''}>${tr(l)}</option>`).join('')}</select>`;else{const input=`<input ${rule?'type="number"':''} ${rule?`min="${rule[0]}" max="${rule[1]}" step="${rule[2]}"`:''} data-section="${esc(section)}" data-key="${esc(key)}" value="${esc(value)}">`;control=unit?`<div class="input-unit">${input}<span class="unit">${unit}</span></div>`:input}return`<div class="field ${extra}"><label>${esc(label)}${control}</label>${help?`<div class="hint">${esc(help)}</div>`:''}</div>`}
|
||
function pluginText(zh,en){return LANG==='zh'?zh:en}
|
||
function renderPlugins(){return `<div class="group"><p>${pluginText('仅发现插件不会接管插件的设备能力。接入后发送监控状态;暂停或断开联动不会停止插件的本地功能。','Discovery does not take control of plugin device capabilities. Connecting sends monitoring state; pausing or disconnecting the link does not stop the plugin’s local functions.')}</p><div class="action-row"><button type="button" id="refreshPlugins" onclick="loadPlugins()">${tr('刷新列表')}</button> <span id="pluginRefreshState" class="hint" role="status" aria-live="polite"></span></div><div id="pluginList"></div></div><div id="pluginEditor"></div>`}
|
||
function fanMetric(plugins){const fans=Object.entries(plugins).filter(([,p])=>p.online&&p.capabilities?.fan&&p.status?.available!==false).map(([id,p])=>{const st=p.status;let value='';if(p.capabilities.fan.rpm&&typeof st.fan_rpm==='number'&&Number.isFinite(st.fan_rpm)&&st.fan_rpm>=0)value=st.fan_rpm+' RPM';else if(st.fan_name&&st.fan_name!=='UNAVAILABLE')value=({OFF:pluginText('关闭','Off'),L1:pluginText('低速','Low'),L2:pluginText('中低速','Medium-low'),L3:pluginText('中速','Medium'),L4:pluginText('高速','High'),MAX:pluginText('全速','Full speed')})[st.fan_name]||st.fan_name;return value?{name:p.name||id,value}:null}).filter(Boolean);return fans.length?`<span> · ${fans.map(f=>esc(pluginText('风扇','Fan')+(fans.length>1?' '+f.name:'')+' '+f.value)).join(' · ')}</span>`:''}
|
||
function pluginSummary(plugins,host){if(!Object.keys(plugins).length)return `<div class="group"><strong>${pluginText('风扇与硬件插件','Fan and hardware plugins')}</strong><p class="muted">${pluginText('尚未接入硬件插件,风扇状态不可用。独立安装并启动插件后,可在插件管理中接入。','No hardware plugin connected; fan status is unavailable. Install and start a plugin independently, then connect it in plugin management.')}</p><button type="button" data-manage-plugin-host="${esc(host)}">${pluginText('管理插件','Manage plugin')}</button></div>`;return Object.entries(plugins).map(([id,p])=>{const st=p.status||{},cap=p.capabilities||{},details=[];if(p.online){if(cap.display)details.push('OLED '+esc(ownerName(st.oled_page||'')));if(cap.rgb)details.push('RGB '+esc(rgbName(st.rgb_mode||'')))}return `<div class="group action-row"><span><strong>${esc(p.name||id)}</strong> · ${p.online?tr('在线'):tr('离线')} ${details.join(' · ')}</span><button type="button" data-manage-plugin-host="${esc(host)}">${pluginText('管理插件','Manage plugin')}</button></div>`}).join('')}
|
||
|
||
let pluginRows=[],pluginDraft=null,pluginSelected='';
|
||
async function loadPlugins(){const list=$('pluginList');if(!list)return;const button=$('refreshPlugins'),feedback=$('pluginRefreshState');if(button?.disabled)return;if(button)button.disabled=true;if(feedback)feedback.textContent=pluginText('正在刷新…','Refreshing…');list.textContent=pluginText('正在发现本机插件…','Discovering local plugins…');try{const data=await get('/api/v1/plugins?host='+encodeURIComponent(currentConfigHost));pluginRows=data.plugins;if(feedback)feedback.textContent=pluginText('刷新完成,发现 '+pluginRows.length+' 个插件 · ','Refreshed, '+pluginRows.length+' plugins found · ')+new Date().toLocaleTimeString();list.innerHTML=pluginRows.map(p=>`<div class="group"><h3>${esc(p.descriptor?.name||p.id)}</h3><div class="hint">${esc(p.socket)} · ${p.online?tr('在线'):tr('离线')} · ${p.linked?(p.paused?pluginText('联动已暂停','Link paused'):pluginText('已接入','Connected')):pluginText('尚未接入','Not connected')}</div>${p.error?`<p class="error">${esc(p.error)}</p>`:''}${p.status?.errors&&Object.keys(p.status.errors).length?`<p class="error">${esc(JSON.stringify(p.status.errors))}</p>`:''}<p>${Object.keys(p.descriptor?.capabilities||{}).map(k=>esc(({fan:pluginText('风扇','Fan'),display:pluginText('屏幕','Display'),rgb:pluginText('灯光','Lighting')})[k]||k)).join(' · ')}</p><div class="toolbar">${p.linked?`<button data-plugin-op="${p.paused?'resume':'pause'}" data-plugin-id="${esc(p.id)}">${p.paused?pluginText('恢复告警联动','Resume alert link'):pluginText('暂停告警联动','Pause alert link')}</button><button data-plugin-op="unlink" data-plugin-id="${esc(p.id)}">${pluginText('断开接入','Disconnect')}</button>`:`<button data-plugin-op="link" data-plugin-id="${esc(p.id)}" ${p.online?'':'disabled'}>${pluginText('接入插件','Connect plugin')}</button>`}<button data-plugin-edit="${esc(p.id)}" ${p.online?'':'disabled'}>${pluginText('插件配置','Plugin settings')}</button></div></div>`).join('')||`<p class="muted">${pluginText('未发现正在运行的硬件插件。刷新只查询插件,不会安装或启动服务。若已安装,请先在目标机器启动插件,再刷新并接入;未接入不影响监控。','No running hardware plugins found. Refresh only queries plugins; it does not install or start services. Start the installed plugin on the target host, then refresh and connect. Monitoring works independently.')}</p>`;list.querySelectorAll('[data-plugin-op]').forEach(b=>b.onclick=()=>pluginAction(b.dataset.pluginId,b.dataset.pluginOp));list.querySelectorAll('[data-plugin-edit]').forEach(b=>b.onclick=()=>editPlugin(b.dataset.pluginEdit))}catch(e){list.textContent=e.message;if(feedback)feedback.textContent=pluginText('刷新失败','Refresh failed')}finally{if(button)button.disabled=false}}
|
||
async function pluginPut(path,payload){return get(path,{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({...payload,host:currentConfigHost})})}
|
||
async function pluginAction(id,operation){try{await pluginPut('/api/v1/plugins',{id,operation});await loadPlugins()}catch(e){alert(e.message)}}
|
||
function capabilityField(section,key,value,spec){const unit=LANG==='zh'?({s:'秒',ms:'毫秒'}[spec.unit]||spec.unit):spec.unit,label=spec.label?.[LANG]||spec.label?.en||key,id=`${section}.${key}`,vis=spec.visible?` data-cap-visible="${esc(JSON.stringify(spec.visible))}"`:'';return `<label class="field"${vis}>${esc(label)}${spec.type==='enum'?`<select data-cap-section="${esc(section)}" data-cap-key="${esc(key)}">${spec.choices.map(x=>`<option value="${esc(x)}" ${String(value)===String(x)?'selected':''}>${esc(pluginChoice(x))}</option>`).join('')}</select>`:`<div class="input-unit"><input type="number" step="${section==='rgb'?1:'any'}" min="${esc(spec.min)}" max="${esc(spec.max)}" data-cap-section="${esc(section)}" data-cap-key="${esc(key)}" value="${esc(value)}"><span class="unit">${esc(unit)}</span></div><span class="hint">${pluginText('范围','Range')} ${spec.min}–${spec.max} ${esc(unit)}</span>`}</label>`}
|
||
function pluginChoice(x){const zh={off:'全灭',solid:'自定义常亮',flash:'自定义闪烁(实验性)',breathe:'内置呼吸',flow:'流水灯',marquee:'跑马灯',rainbow:'彩虹灯',colorful:'炫彩灯',red:'红色',green:'绿色',blue:'蓝色',yellow:'黄色',purple:'紫色',cyan:'青色',white:'白色'};return LANG==='zh'?(zh[x]||x):x}
|
||
function capabilityFields(section,fields,values){const seen=new Set();return Object.entries(fields).map(([key,spec])=>{if(!spec.color_group)return capabilityField(section,key,values[key],spec);if(seen.has(spec.color_group))return '';seen.add(spec.color_group);const members=Object.entries(fields).filter(([,v])=>v.color_group===spec.color_group),hex='#'+['r','g','b'].map(c=>Number(values[members.find(([,v])=>v.channel===c)[0]]).toString(16).padStart(2,'0')).join('');return `<div class="group span2" data-cap-visible="${esc(JSON.stringify(spec.visible||{}))}"><div style="display:grid;grid-template-columns:52px repeat(3,minmax(0,1fr));gap:10px;align-items:start"><label style="text-align:center">${pluginText('颜色','Color')}<input type="color" data-cap-color="${esc(spec.color_group)}" value="${hex}" aria-label="${pluginText('选择颜色','Choose color')}" style="width:38px;height:34px;padding:0;margin-top:5px"></label>${members.map(([k,v])=>capabilityField(section,k,values[k],{...v,visible:null})).join('')}</div></div>`}).join('')}
|
||
async function editPlugin(id){try{pluginSelected=id;pluginDraft=await get('/api/v1/plugin/config?host='+encodeURIComponent(currentConfigHost)+'&id='+encodeURIComponent(id));const data=pluginDraft,editor=$('pluginEditor');editor.innerHTML=`<div class="group"><h3>${esc(id)}</h3><p class="hint">${pluginText('配置由插件保存并立即应用,不重启监控。','Settings are saved and applied by the plugin without restarting monitoring.')}</p>${(data.notes||[]).map(n=>`<p class="hint">${esc(n[LANG]||n.en||'')}</p>`).join('')}${Object.entries(data.schema).map(([section,fields])=>`<div class="group"><h3>${({fan:pluginText('风扇','Fan'),oled:pluginText('屏幕','Display'),rgb:pluginText('灯光','Lighting')})[section]||esc(section)}</h3><div class="fields">${capabilityFields(section,fields,data.config[section])}</div></div>`).join('')}<div class="plugin-actions"><button id="savePlugin">${pluginText('保存插件配置','Save plugin settings')}</button> <span id="pluginSaveState"></span></div></div>`;editor.oninput=e=>{if(e.target.dataset.capColor){const group=e.target.dataset.capColor,hex=e.target.value;['r','g','b'].forEach((c,i)=>{editor.querySelector(`[data-cap-key="${group}_${c}"]`).value=parseInt(hex.slice(1+2*i,3+2*i),16)})}for(const picker of editor.querySelectorAll('[data-cap-color]')){picker.value='#'+['r','g','b'].map(c=>Math.max(0,Math.min(255,Number(editor.querySelector(`[data-cap-key="${picker.dataset.capColor}_${c}"]`).value))).toString(16).padStart(2,'0')).join('')}updatePluginVisibility()};updatePluginVisibility();$('savePlugin').onclick=savePluginConfig}catch(e){$('pluginEditor').textContent=e.message}}
|
||
function updatePluginVisibility(){document.querySelectorAll('[data-cap-visible]').forEach(el=>{const rules=JSON.parse(el.dataset.capVisible);el.classList.toggle('hidden',!Object.entries(rules).every(([key,values])=>values.includes(document.querySelector(`[data-cap-key="${key}"]`)?.value)))})}
|
||
async function savePluginConfig(){const updates={};for(const el of document.querySelectorAll('[data-cap-key]')){if(!el.reportValidity())return;const s=el.dataset.capSection,k=el.dataset.capKey;(updates[s]??={})[k]=el.value}try{await pluginPut('/api/v1/plugin/config',{id:pluginSelected,updates});$('pluginSaveState').textContent=pluginText('保存成功','Saved')}catch(e){$('pluginSaveState').textContent=e.message}}
|
||
document.addEventListener('click',async e=>{const b=e.target.closest('[data-manage-plugin-host]');if(!b)return;currentConfigHost=b.dataset.managePluginHost;await ensureHosts();$('configHost').value=currentConfigHost;document.querySelector('[data-tab="config"]').click();await loadConfig();document.querySelector('[data-config-section="plugins"]')?.click()});
|
||
|
||
let deviceIdentity={},bulkOriginal={},bulkSource='';
|
||
function renderDevice(values){return `<div class="group"><div class="fields">${Object.entries(values).map(([key,raw])=>{const automatic=!raw||raw.toLowerCase()==='auto',value=automatic?(deviceIdentity[key==='identifier'?'id':'name']||deviceIdentity.hostname||''):raw;return field('device',key,value).replace('data-section="device"',`data-device-auto="${automatic}" data-section="device"`)}).join('')}</div><button type="button" onclick="resetDeviceDefaults()">${tr('恢复设备默认值')}</button><p class="hint">${pluginText('默认值跟随本机主机名;修改后点击保存到当前机器。','Defaults follow the device hostname. Save to the current host to apply changes.')}</p></div>`}
|
||
function resetDeviceDefaults(){document.querySelectorAll('[data-section="device"]').forEach(el=>{el.value=deviceIdentity.hostname||'';el.dataset.deviceAuto='true'});markChanged()}
|
||
async function loadMachines(){try{const registry=await get('/api/v1/hosts');fleetHosts=registry.hosts;bulkSource=fleetHosts.some(h=>h.id===bulkSource)?bulkSource:(fleetHosts[0]?.id||'');$('machineContent').innerHTML=`${renderHosts(Object.fromEntries(fleetHosts.map(h=>[h.id,h.url])))}<div class="group"><h2>${tr('批量同步')}</h2><label class="host-select">${tr('来源机器')}<select id="bulkSource">${fleetHosts.map(h=>`<option value="${esc(h.id)}" ${h.id===bulkSource?'selected':''}>${esc(h.name||h.id)}</option>`).join('')}</select></label><div id="machineBulk"></div></div>`;$('saveHost').onclick=saveHost;document.querySelectorAll('[data-delete-host]').forEach(b=>b.onclick=()=>deleteHost(b.dataset.deleteHost));document.querySelectorAll('[data-edit-host]').forEach(b=>b.onclick=()=>{const h=fleetHosts.find(h=>h.id===b.dataset.editHost);$('hostId').value=h.id;$('hostUrl').value=h.url;$('hostTokenInput').value=''});document.querySelectorAll('[data-check-host]').forEach(b=>b.onclick=async()=>{b.disabled=true;try{await get('/api/v1/hosts/check',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({id:b.dataset.checkHost})});$('hostMessage').textContent=pluginText('连接正常','Connection successful')}catch(e){$('hostMessage').textContent=e.message}finally{b.disabled=false}});$('bulkSource').onchange=()=>{bulkSource=$('bulkSource').value;loadMachineBulk()};if(bulkSource)await loadMachineBulk();else $('machineBulk').textContent=pluginText('先添加设备,再选择配置来源与目标。','Add devices before selecting a configuration source and targets.')}catch(e){$('machineContent').textContent=e.message}}
|
||
async function loadMachineBulk(){const source=bulkSource;$('machineBulk').textContent=pluginText('正在读取配置…','Loading configuration…');try{const d=await get('/api/v1/config?host='+encodeURIComponent(source));if(source!==bulkSource)return;bulkOriginal=d.config;$('machineBulk').innerHTML=renderBulkSync();bindBulkSync()}catch(e){$('machineBulk').textContent=e.message}}
|
||
|
||
function renderGeneral(s,v){if(s==='device')return renderDevice(v);const [t,d]=sectionMeta[s]||[s,''];return`<div class="group"><div class="group-head"><div><h3>${tr(t)}</h3><div class="hint">${tr(d)}</div></div></div><div class="fields">${Object.entries(v).map(([k,x])=>field(s,k,x)).join('')||'<div class="muted">暂无配置项</div>'}</div></div>`}
|
||
function renderServices(){return`<div class="group"><div class="group-head"><div><h3>当前系统服务</h3><div class="hint">点击“加入监控”会自动填写下方表单;也可以手动输入尚未安装的未来服务。</div></div><button id="refreshServices">刷新列表</button></div><input class="service-search" id="serviceSearch" placeholder="搜索服务名称或说明"><div class="service-list rows" id="systemServices"><div class="muted">正在读取 systemd 服务…</div></div></div><div class="group"><h3>已监控服务</h3><div class="hint">“暂停监控”只停止状态检查,不影响服务运行;“停止服务”才会操作 systemd。</div><div class="rows" id="monitoredServices"></div></div><div class="group"><h3 id="serviceFormTitle">新建服务监控</h3><div class="service-form"><label>OLED 显示名称<input id="monitorName" maxlength="64" placeholder="例如:DOCKER"></label><label>systemd 服务名称<input id="monitorTarget" maxlength="128" placeholder="例如:docker.service"></label><label class="full">备注说明<input id="monitorNote" maxlength="120" placeholder="例如:家庭相册容器服务"></label><div class="full action-row"><button id="saveMonitor">保存监控</button> <button id="resetMonitor">清空</button> <span id="serviceMessage" class="muted"></span></div></div></div>`}
|
||
function renderHosts(v){return `<div class="group"><h3>${pluginText('设备列表','Devices')}</h3><div class="rows">${Object.entries(v).map(([id,url])=>`<div class="row"><span><b>${esc(id)}</b><br><span class="muted">${esc(url)}</span></span><div class="action-row"><button data-check-host="${esc(id)}">${pluginText('检测连接','Test connection')}</button><button data-edit-host="${esc(id)}">${tr('编辑')}</button><button class="danger" data-delete-host="${esc(id)}">${tr('删除')}</button></div></div>`).join('')||pluginText('尚未添加设备','No devices added')}</div></div><div class="group"><h3>${tr('添加或更新机器')}</h3><div class="service-form"><label>${tr('机器标识')}<input id="hostId" maxlength="64"></label><label>${tr('节点地址')}<input id="hostUrl" placeholder="http://192.168.1.20:6001"></label><label class="full">${pluginText('设备 API 令牌(修改时留空保留原值)','Device API token (leave blank when editing to keep existing)')}<input id="hostTokenInput" type="password" autocomplete="new-password"></label><div class="full action-row"><button id="saveHost">${tr('保存机器')}</button><span id="hostMessage"></span></div></div></div>`}
|
||
function renderBulkTargets(){return `<div class="group"><h3>${tr('目标机器')}</h3><div class="hint">${tr('机器标识和机器列表不会参与同步。')}</div>${fleetHosts.filter(h=>h.id!==bulkSource).map(h=>`<label class="action-row"><input type="checkbox" data-bulk-target="${esc(h.id)}">${esc(h.name||h.id)}</label>`).join('')||pluginText('没有其他设备','No other devices')}</div>`}
|
||
function bulkValue(section,key,value){const item=options[`${section}.${key}`]?.find(([v])=>v===String(value));return item?tr(item[1]):value}
|
||
function renderBulkSync(){const groups=bulkSections.filter(s=>bulkOriginal[s]&&Object.keys(bulkOriginal[s]).length).map(s=>`<div class="group bulk-section"><label class="bulk-section-title"><input type="checkbox" data-bulk-section="${esc(s)}">${tr(sectionMeta[s]?.[0]||s)}</label><div class="bulk-items">${Object.entries(bulkOriginal[s]).map(([k,v])=>`<label class="bulk-item"><input type="checkbox" data-bulk-item="${esc(s)}.${esc(k)}"><span>${esc(tr(labels[`${s}.${k}`]||k))}<small class="bulk-value">${esc(bulkValue(s,k,v))}</small></span></label>`).join('')}</div></div>`).join('');return`<div class="group"><h3>${tr('来源机器')}</h3><div>${esc(fleetHosts.find(h=>h.id===bulkSource)?.name||bulkSource)} · ${esc(bulkSource)}</div></div>${renderBulkTargets()}<div class="bulk-tools"><button id="bulkAll">${tr('全部选择')}</button><button id="bulkNone">${tr('全部取消')}</button><button id="bulkInvert">${tr('反选')}</button><span class="muted" id="bulkCount"></span></div><div><h3>${tr('配置项')}</h3>${groups}</div><div class="savebar"><button id="bulkApply">${tr('开始批量同步')}</button> <span id="bulkState" class="muted"></span></div>`}
|
||
function updateBulkSelection(){document.querySelectorAll('[data-bulk-section]').forEach(parent=>{const items=[...document.querySelectorAll(`[data-bulk-item^="${parent.dataset.bulkSection}."]`)];parent.checked=items.length>0&&items.every(x=>x.checked);parent.indeterminate=items.some(x=>x.checked)&&!parent.checked});const n=document.querySelectorAll('[data-bulk-item]:checked').length;if($('bulkCount'))$('bulkCount').textContent=LANG==='zh'?`已选择 ${n} 项`:`${n} settings selected`}
|
||
function bindBulkSync(){if(!$('bulkApply'))return;document.querySelectorAll('[data-bulk-section]').forEach(x=>x.onchange=()=>{document.querySelectorAll(`[data-bulk-item^="${x.dataset.bulkSection}."]`).forEach(i=>i.checked=x.checked);updateBulkSelection()});document.querySelectorAll('[data-bulk-item]').forEach(x=>x.onchange=updateBulkSelection);$('bulkAll').onclick=()=>{document.querySelectorAll('[data-bulk-item]').forEach(x=>x.checked=true);updateBulkSelection()};$('bulkNone').onclick=()=>{document.querySelectorAll('[data-bulk-item]').forEach(x=>x.checked=false);updateBulkSelection()};$('bulkInvert').onclick=()=>{document.querySelectorAll('[data-bulk-item]').forEach(x=>x.checked=!x.checked);updateBulkSelection()};$('bulkApply').onclick=applyBulkConfig;updateBulkSelection()}
|
||
function updateConditional(){const alertMode=document.querySelector('[data-key="alert_mode"]')?.value||'breathe';document.querySelectorAll('.alert-flash').forEach(e=>e.classList.toggle('hidden',alertMode!=='flash'));document.querySelectorAll('.alert-breathe').forEach(e=>e.classList.toggle('hidden',alertMode!=='breathe'));const mode=document.querySelector('[data-section="led"][data-key="normal_mode"]')?.value||'off';document.querySelectorAll('.normal-field').forEach(x=>x.classList.toggle('hidden',!x.dataset.modes.split(' ').includes(mode)));updateSwatches()}function updateSwatches(){document.querySelectorAll('[data-swatch]').forEach(s=>{const p=s.dataset.swatch,read=c=>Math.max(0,Math.min(255,Number(document.querySelector(`[data-section="led"][data-key="${p}_${c}"]`)?.value||0))),hex='#'+['r','g','b'].map(c=>Math.round(read(c)).toString(16).padStart(2,'0')).join('');s.style.background=hex;const picker=document.querySelector(`[data-color-prefix="${p}"]`);if(picker)picker.value=hex})}
|
||
function renderConfig(){const ss=sectionOrder.filter(s=>original[s]||s==='plugins');$('configMenu').innerHTML=ss.map((s,i)=>`<button class="${i===0?'active':''}" data-config-section="${s}">${tr(sectionMeta[s]?.[0]||s)}</button>`).join('');$('configRows').innerHTML=ss.map((s,i)=>`<section class="config-page ${i===0?'active':''}" data-config-page="${s}"><h2>${tr(sectionMeta[s]?.[0]||s)}</h2><p class="muted">${tr(sectionMeta[s]?.[1]||'')}</p>${s==='bulk_sync'?renderBulkSync():s==='plugins'?renderPlugins():s==='services'?renderServices():s==='hosts'?renderHosts(original[s]):renderGeneral(s,original[s])}</section>`).join('');$('configMenu').onclick=e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('[data-config-section]').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-config-page]').forEach(x=>x.classList.toggle('active',x.dataset.configPage===b.dataset.configSection));$('mainSavebar').classList.toggle('hidden',['bulk_sync','plugins'].includes(b.dataset.configSection));if(b.dataset.configSection==='services')loadServices();if(b.dataset.configSection==='plugins')loadPlugins()};$('configRows').oninput=e=>{if(e.target.matches('[data-color-prefix]')){const p=e.target.dataset.colorPrefix,v=e.target.value;['r','g','b'].forEach((c,i)=>{document.querySelector(`[data-section="led"][data-key="${p}_${c}"]`).value=parseInt(v.slice(1+i*2,3+i*2),16)})}if(e.target.matches('[data-section="led"],[data-color-prefix]'))updateConditional();if(e.target.matches('[data-section]')){if(e.target.dataset.section==='device')e.target.dataset.deviceAuto='false';markChanged()}};$('refreshServices')?.addEventListener('click',loadServices);$('serviceSearch')?.addEventListener('input',renderServiceLists);$('saveMonitor')?.addEventListener('click',saveMonitor);$('resetMonitor')?.addEventListener('click',resetMonitorForm);$('saveHost')?.addEventListener('click',saveHost);document.querySelectorAll('[data-delete-host]').forEach(b=>b.onclick=()=>deleteHost(b.dataset.deleteHost));bindBulkSync();updateConditional()}
|
||
function markChanged(){let n=0;document.querySelectorAll('#configRows [data-section]').forEach(i=>{if((i.dataset.deviceAuto==='true'?'auto':i.value)!==original[i.dataset.section][i.dataset.key])n++});$('saveState').textContent=n?(LANG==='zh'?`有 ${n} 项尚未保存`:`${n} unsaved changes`):tr('尚未修改');$('saveState').className=n?'':'muted'}
|
||
tabs.forEach(b=>b.onclick=()=>{tabs.forEach(x=>x.classList.toggle('active',x===b));['overview','logs','config','machines'].forEach(id=>$(id).classList.toggle('hidden',id!==b.dataset.tab));if(b.dataset.tab==='machines')loadMachines();if(b.dataset.tab==='logs')loadLogs();if(b.dataset.tab==='config')loadConfig()});async function get(url,opt={}){const r=await fetch(url,{...opt,headers:{...(opt.headers||{}),Authorization:'Bearer '+hostToken()}}),text=await r.text();let j;try{j=JSON.parse(text)}catch{throw Error(pluginText('服务器返回异常','Invalid server response'))}if(r.status===401){$('managerLogin').classList.remove('hidden');throw Error(pluginText('请连接管理中心:输入管理中心令牌','Enter the manager token to connect'))}if(!r.ok)throw Error(j.error||'Request failed');return j}function rows(target,items,empty='暂无数据'){$(target).innerHTML=items.length?items.join(''):`<div class="muted">${empty}</div>`}
|
||
let serviceCache=[],editingServiceName='',serviceReadOnly=false;function tokenHeaders(){return {'Content-Type':'application/json'}}
|
||
async function loadServices(){if(!$('systemServices'))return;try{serviceReadOnly=false;const d=await get(`/api/v1/services?host=${encodeURIComponent(currentConfigHost)}`);serviceCache=d.services;renderServiceLists()}catch(e){$('systemServices').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
||
function renderServiceLists(){if(!$('systemServices'))return;const q=$('serviceSearch').value.trim().toLowerCase(),filtered=serviceCache.filter(s=>!q||`${s.unit} ${s.description} ${s.note}`.toLowerCase().includes(q));rows('systemServices',filtered.map(s=>`<div class="row"><span><b>${esc(s.unit)}</b><br><span class="muted">${esc(s.description||'暂无服务说明')} · ${esc(s.active==='active'?'正在运行':'未运行')}</span></span><span class="row-actions"><span class="badge ${s.monitored&&!s.paused?'good':''}">${s.monitored?(s.paused?'已暂停':'已监控'):'未监控'}</span><button data-select-service="${esc(s.unit)}">${s.monitored?'编辑监控':'加入监控'}</button></span></div>`),'没有匹配的服务');const monitored=serviceCache.filter(s=>s.monitored);rows('monitoredServices',monitored.map(s=>`<div class="row"><span><b>${esc(s.monitor_name)}</b> <span class="badge ${s.paused?'':s.active==='active'?'good':'bad'}">${s.paused?'监控已暂停':s.active==='active'?'运行中':'服务已停止'}</span><br><span class="muted">${esc(s.unit)}</span><br><span class="muted">备注:${esc(s.note||'未填写')}</span></span><span class="row-actions"><button data-monitor-action="${s.paused?'resume':'pause'}" data-unit="${esc(s.unit)}">${s.paused?'恢复监控':'暂停监控'}</button><button data-service-action="${s.active==='active'?'stop':'start'}" data-unit="${esc(s.unit)}" ${s.protected&&s.active==='active'?'disabled title="API 服务不能停止自身"':''}>${s.active==='active'?'停止服务':'启动服务'}</button><button data-select-service="${esc(s.unit)}">编辑</button><button class="danger" data-delete-monitor="${esc(s.unit)}">删除监控</button></span></div>`),'尚未监控任何服务');if(serviceReadOnly)document.querySelectorAll('#configRows button').forEach(b=>b.disabled=true);document.querySelectorAll('[data-select-service]').forEach(b=>b.onclick=()=>selectService(b.dataset.selectService));document.querySelectorAll('[data-monitor-action]').forEach(b=>b.onclick=()=>monitorAction(b.dataset.unit,b.dataset.monitorAction));document.querySelectorAll('[data-service-action]').forEach(b=>b.onclick=()=>serviceAction(b.dataset.unit,b.dataset.serviceAction));document.querySelectorAll('[data-delete-monitor]').forEach(b=>b.onclick=()=>deleteMonitor(b.dataset.deleteMonitor))}
|
||
function selectService(unit){const s=serviceCache.find(x=>x.unit===unit);if(!s)return;editingServiceName=s.monitor_name||'';$('monitorTarget').value=s.unit;$('monitorName').value=s.monitor_name||s.unit.replace(/\.service$/,'').replace(/[^A-Za-z0-9_.-]/g,'_').slice(0,16).toUpperCase();$('monitorNote').value=s.note||s.description||'';$('serviceFormTitle').textContent=s.monitored?'编辑服务监控':'新建服务监控';$('serviceMessage').textContent='已自动填写,可继续修改。';$('monitorName').scrollIntoView({behavior:'smooth',block:'center'})}
|
||
function resetMonitorForm(){editingServiceName='';$('monitorName').value='';$('monitorTarget').value='';$('monitorNote').value='';$('serviceFormTitle').textContent='新建服务监控';$('serviceMessage').textContent='';$('serviceMessage').className='muted'}
|
||
async function saveMonitor(){const body={host:currentConfigHost,operation:'save',name:$('monitorName').value.trim(),target:$('monitorTarget').value.trim(),note:$('monitorNote').value.trim(),previous_name:editingServiceName};try{$('serviceMessage').textContent='正在保存…';await get('/api/v1/services',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify(body)});resetMonitorForm();await loadServices();$('serviceMessage').textContent='保存成功';$('serviceMessage').className='success'}catch(e){$('serviceMessage').textContent=e.message;$('serviceMessage').className='error'}}
|
||
async function deleteMonitor(unit){const s=serviceCache.find(x=>x.unit===unit);if(!s||!confirm(LANG==='zh'?`确定删除 ${s.monitor_name} 的监控吗?\n不会删除或停止 ${unit}。`:`Remove monitoring for ${s.monitor_name}?\nThis will not remove or stop ${unit}.`))return;try{await get('/api/v1/services',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({host:currentConfigHost,operation:'delete',name:s.monitor_name,target:s.unit,note:''})});await loadConfig();document.querySelector('[data-config-section="services"]')?.click()}catch(e){alert(e.message)}}
|
||
async function monitorAction(unit,action){const s=serviceCache.find(x=>x.unit===unit);if(!s)return;try{await get('/api/v1/services',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({host:currentConfigHost,operation:action,name:s.monitor_name,target:s.unit,note:s.note||''})});await loadServices()}catch(e){alert(e.message)}}
|
||
async function serviceAction(unit,action){if(!confirm(LANG==='zh'?`确定${action==='start'?'启动服务':'停止服务'} ${unit} 吗?\n这会直接执行 systemctl ${action}。`:`${action==='start'?'Start':'Stop'} ${unit}?\nThis runs systemctl ${action} directly.`))return;try{await get('/api/v1/services/control',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({host:currentConfigHost,unit,action})});setTimeout(loadServices,700)}catch(e){alert(e.message)}}
|
||
function bytes(value){const n=Number(value||0);if(!n)return'--';const units=['B','KB','MB','GB','TB'];let x=n,i=0;while(x>=1024&&i<units.length-1){x/=1024;i++}return`${x>=100||i===0?x.toFixed(0):x.toFixed(1)} ${units[i]}`}
|
||
function meter(percent){const p=Math.max(0,Math.min(100,Number(percent||0)));return`<div class="progress"><span style="width:${p}%"></span></div>`}
|
||
function hostCard(entry){const h=entry.host;if(!entry.online)return`<article class="panel host-panel offline-card"><div class="host-head"><div><h2>${esc(h.name||h.id)}</h2><div class="device-details"><span>ID ${esc(h.id)}</span><span>${esc(h.url||tr('本机'))}</span></div></div><span class="badge bad">${tr('离线')}</span></div><div class="error">${esc(entry.error||tr('连接失败'))}</div></article>`;const s=entry.status,y=s.system||{},n=s.network||{},d=s.display||{},v=s.device||{},m=s.machine||h,signal=n.signal_percent,memUsed=y.memory_used_bytes,memTotal=y.memory_total_bytes,diskUsed=y.disk_used_bytes,diskTotal=y.disk_total_bytes;return`<article class="panel host-panel"><div class="host-head"><div><h2>${esc(m.name||h.name||m.id)}</h2><div class="device-details"><span>ID ${esc(m.id||h.id)}</span><span>${LANG==='zh'?'主机名':'Hostname'} ${esc(m.hostname||v.hostname||'--')}</span><span>${esc(v.manufacturer||'')} ${esc(v.model||'--')}</span><span>${LANG==='zh'?'序列号':'Serial'} ${esc(v.serial||'--')}</span><span>${LANG==='zh'?'硬件版本':'Revision'} ${esc(v.revision||'--')}</span><span>${esc(v.os||'--')}</span><span>${LANG==='zh'?'内核':'Kernel'} ${esc(v.kernel||'--')}</span><span>${esc(v.architecture||'--')} · CPU ${esc(v.cpu_count||'--')}</span><span>${LANG==='zh'?'程序版本':'Agent'} ${esc(s.version||'--')}</span></div></div><span class="badge good">${tr('在线')}</span></div><div class="host-grid"><div class="metric"><div class="muted">${tr('处理器负载')}</div><div class="value">${esc(y.cpu_percent)}%</div>${meter(y.cpu_percent)}<div class="muted">${LANG==='zh'?'频率':'Frequency'} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C${fanMetric(s.plugins||{})}</div></div><div class="metric"><div class="muted">${tr('内存占用')}</div><div class="value">${esc(y.memory_percent)}%</div>${meter(y.memory_percent)}<div class="muted">${bytes(memUsed)} / ${bytes(memTotal)} · ${LANG==='zh'?'可用':'Available'} ${bytes(y.memory_available_bytes)}</div></div><div class="metric"><div class="muted">${tr('磁盘占用')}</div><div class="value">${esc(y.disk_percent)}%</div>${meter(y.disk_percent)}<div class="muted">${bytes(diskUsed)} / ${bytes(diskTotal)} · ${LANG==='zh'?'剩余':'Free'} ${bytes(y.disk_free_bytes)}</div></div><div class="metric"><div class="muted">${tr('主网络')}</div><div class="value">${n.kind==='WIF'&&signal!==null&&signal!==undefined?`${esc(signal)}%`:esc(n.kind||'--')}</div>${n.kind==='WIF'?meter(signal):''}<div class="muted">${esc(n.interface||'--')} · ${n.kind==='WIF'?`${esc(n.metric)} dBm · `:''}${esc(n.ip_label)} ${esc(n.ip)} · ${n.internet?(LANG==='zh'?'互联网正常':'Internet online'):(LANG==='zh'?'互联网断开':'Internet down')}</div></div><div class="metric"><div class="muted">${tr('显示状态')}</div><div class="value">${esc(ownerName(d.owner))}</div><div class="muted">${LANG==='zh'?'告警状态独立于硬件插件':'Alert state is independent of hardware plugins'}</div></div><div class="metric"><div class="muted">${tr('当前告警')}</div><div class="value">${(d.alerts||[]).length}</div><div class="muted">${(d.alerts||[]).map(a=>esc(ownerName(a.id))).join(' · ')||tr('无告警')}</div></div></div>${pluginSummary(s.plugins||{},h.id)}<div class="group"><h3>${tr('服务与进程监控')}</h3><div class="rows">${(s.watches||[]).map(w=>`<div class="row"><span>${esc(w.name)} · ${esc(w.target)}</span><span class="badge ${w.up?'good':'bad'}">${tr(w.up?'正常':'停止')}</span></div>`).join('')||`<div class="muted">${tr('暂无数据')}</div>`}</div></div></article>`}
|
||
function renderHostCards(entries){const root=$('hostCards'),seen=new Set;if(!entries.length){root.innerHTML='<div class="panel muted">'+pluginText('尚未添加设备,请前往机器管理。','No devices added. Open Machine management to add one.')+'</div>';return}entries.forEach((entry,index)=>{const id=String(entry.host.id),template=document.createElement('template');template.innerHTML=hostCard(entry);const next=template.content.firstElementChild;next.dataset.hostId=id;let current=[...root.children].find(x=>x.dataset.hostId===id);if(current){current.className=next.className;current.innerHTML=next.innerHTML;if(root.children[index]!==current)root.insertBefore(current,root.children[index]||null)}else root.insertBefore(next,root.children[index]||null);seen.add(id)});[...root.children].forEach(x=>{if(!seen.has(x.dataset.hostId))x.remove()})}
|
||
async function loadStatus(){try{const fleet=await get('/api/v1/fleet/status'),online=fleet.hosts.filter(x=>x.online);$('dot').classList.toggle('ok',online.length>0);$('liveText').textContent=LANG==='zh'?`${online.length}/${fleet.hosts.length} 在线`:`${online.length}/${fleet.hosts.length} online`;$('identity').textContent=LANG==='zh'?`多机监控 · ${fleet.hosts.length} 台设备 · ${new Date(fleet.timestamp*1000).toLocaleTimeString()}`:`Fleet monitoring · ${fleet.hosts.length} hosts · ${new Date(fleet.timestamp*1000).toLocaleTimeString()}`;renderHostCards(fleet.hosts);fleetHosts=fleet.hosts.map(x=>x.host);refreshHostSelectors()}catch(e){$('dot').classList.remove('ok');$('liveText').textContent=tr('离线');$('identity').textContent=e.message}}
|
||
const severityNames={DEBUG:'调试',INFO:'信息',NOTICE:'注意',WARN:'警告',ERROR:'错误',CRITICAL:'严重',ALERT:'紧急',EMERGENCY:'紧急'};let logState={sort:'timestamp',order:'desc',offset:0,limit:100,total:0};
|
||
function localDateValue(d){return`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}
|
||
function setLogRange(range){const now=new Date(),today=new Date(now.getFullYear(),now.getMonth(),now.getDate()),start=new Date(today),end=new Date(today);if(range==='all'){$('logStart').value='';$('logEnd').value='';return}if(range==='yesterday'){start.setDate(start.getDate()-1);end.setDate(end.getDate()-1)}else if(range==='week'){start.setDate(start.getDate()-((start.getDay()+6)%7))}else if(range==='month'){start.setDate(1)}$('logStart').value=localDateValue(start);$('logEnd').value=localDateValue(end)}
|
||
function logTimestamp(value,end=false){if(!value)return'';const d=new Date(`${value}T00:00:00`);if(end)d.setDate(d.getDate()+1);return Math.floor(d.getTime()/1000)-(end?1:0)}
|
||
async function loadLogs(reset=true){if(reset)logState.offset=0;try{const p=new URLSearchParams({limit:logState.limit,offset:logState.offset,sort:logState.sort,order:logState.order}),since=logTimestamp($('logStart').value),until=logTimestamp($('logEnd').value,true),severity=$('logSeverity').value,event=$('logEvent').value,machine=$('logMachine').value,search=$('logSearch').value.trim();if(since)p.set('since',since);if(until)p.set('until',until);if(severity)p.set('severity',severity);if(event)p.set('event',event);if(machine)p.set('machine',machine);if(search)p.set('search',search);const d=await get(`/api/v1/fleet/logs?${p}`);logState.total=d.total;const selectedEvent=$('logEvent').value,selectedMachine=$('logMachine').value;$('logEvent').innerHTML=`<option value="">${tr('全部事件')}</option>`+d.event_types.map(x=>`<option value="${esc(x)}" ${x===selectedEvent?'selected':''}>${esc(eventName(x))}</option>`).join('');$('logMachine').innerHTML=`<option value="">${LANG==='zh'?'全部机器':'All hosts'}</option>`+d.machines.map(x=>`<option value="${esc(x.id)}" ${x.id===selectedMachine?'selected':''}>${esc(x.name)} · ${esc(x.id)}</option>`).join('');$('logRows').innerHTML=d.logs.length?d.logs.map(x=>`<tr><td>${esc(new Date(x.timestamp/1000).toLocaleString())}</td><td>${esc(x.machine_name||x.machine_id)}</td><td><span class="badge ${['ERROR','CRITICAL','ALERT','EMERGENCY'].includes(x.severity)?'bad':''}">${esc(tr(severityNames[x.severity]||x.severity))}</span></td><td>${esc(eventName(x.event))}</td><td class="log-message">${esc(x.message)}</td></tr>`).join(''):`<tr><td colspan="5" class="muted">${tr('没有符合条件的日志')}</td></tr>`;$('logSummary').textContent=LANG==='zh'?`共 ${d.total} 条 · 数据来源:各机器 systemd journal`:`${d.total} events · Source: each host's systemd journal`;if(d.errors?.length)$('logSummary').textContent+=' · '+pluginText('部分设备查询失败或结果受限','Some devices failed or results were limited');$('logPage').textContent=d.total?`${d.offset+1}–${Math.min(d.offset+d.limit,d.total)} / ${d.total}`:(LANG==='zh'?'0 条':'0 events');$('logPrev').disabled=d.offset===0;$('logNext').disabled=d.offset+d.limit>=d.total;document.querySelectorAll('[data-log-sort]').forEach(b=>{const names={timestamp:LANG==='zh'?'时间':'Time',machine:LANG==='zh'?'机器':'Host',severity:LANG==='zh'?'程度':'Severity',event:LANG==='zh'?'事件类型':'Event type',message:LANG==='zh'?'内容':'Message'},name=names[b.dataset.logSort];b.textContent=name+(b.dataset.logSort===logState.sort?(logState.order==='asc'?' ↑':' ↓'):'')})}catch(e){$('logRows').innerHTML=`<tr><td colspan="5" class="error">${esc(e.message)}</td></tr>`}}
|
||
function hostToken(){return sessionStorage.getItem('pigwayManagerToken')||''}
|
||
function refreshHostSelectors(){if(!fleetHosts.length){$('configHost').innerHTML='';currentConfigHost='';return}const current=$('configHost').value||currentConfigHost||fleetHosts[0]?.id;$('configHost').innerHTML=fleetHosts.map(h=>`<option value="${esc(h.id)}" ${h.id===current?'selected':''}>${esc(h.name||h.id)} · ${esc(h.id)}</option>`).join('');currentConfigHost=$('configHost').value}
|
||
function changedUpdates(){const updates={};document.querySelectorAll('#configRows [data-section]').forEach(i=>{if(original[i.dataset.section]&&(i.dataset.deviceAuto==='true'?'auto':i.value)!==original[i.dataset.section][i.dataset.key]){(updates[i.dataset.section]??={})[i.dataset.key]=i.dataset.deviceAuto==='true'?'auto':i.value}});return updates}
|
||
async function ensureHosts(){if(fleetHosts.length)return;const d=await get('/api/v1/hosts');fleetHosts=d.hosts;refreshHostSelectors()}
|
||
async function loadConfig(){try{await ensureHosts();currentConfigHost=$('configHost').value||currentConfigHost||fleetHosts[0]?.id;if(!currentConfigHost){$('configRows').textContent=pluginText('请先在机器管理中添加设备','Add a device in Machine management first');$('configMenu').innerHTML='';$('mainSavebar').classList.add('hidden');return}$('mainSavebar').classList.remove('hidden');const d=await get(`/api/v1/config?host=${encodeURIComponent(currentConfigHost)}`);original=d.config;deviceIdentity=d.machine||{};renderConfig();markChanged()}catch(e){$('configRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
||
async function saveHost(){try{await get('/api/v1/hosts',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({operation:'save',id:$('hostId').value.trim(),url:$('hostUrl').value.trim(),token:$('hostTokenInput').value.trim()})});await loadMachines();refreshHostSelectors()}catch(e){$('hostMessage').textContent=e.message}}
|
||
async function deleteHost(id){if(!confirm(pluginText('移除设备 ','Remove device ')+id+'?'))return;try{await get('/api/v1/hosts',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify({operation:'delete',id})});await loadMachines();refreshHostSelectors()}catch(e){alert(e.message)}}
|
||
async function applyConfig(){const updates=changedUpdates();if(!Object.keys(updates).length){$('saveState').textContent=tr('没有需要保存的修改');return}const target=currentConfigHost;try{$('saveConfig').disabled=true;$('saveState').textContent=tr('正在保存并应用…');const d=await get('/api/v1/fleet/config',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${hostToken()}`},body:JSON.stringify({targets:[target],updates,})}),failed=d.results.filter(x=>!x.ok);$('saveState').textContent=failed.length?failed[0].error:(LANG==='zh'?'保存成功':'Saved');$('saveState').className=failed.length?'error':'success';if(!failed.length)setTimeout(loadConfig,1200)}catch(e){$('saveState').textContent=e.message;$('saveState').className='error'}finally{$('saveConfig').disabled=false}}
|
||
async function applyBulkConfig(){const state=$('bulkState'),button=$('bulkApply'),targets=[...document.querySelectorAll('[data-bulk-target]:checked')].map(x=>x.dataset.bulkTarget),selected=[...document.querySelectorAll('[data-bulk-item]:checked')].map(x=>x.dataset.bulkItem);if(!targets.length){state.textContent=tr('请选择至少一台目标机器');state.className='error';return}if(!selected.length){state.textContent=tr('请选择至少一个配置项');state.className='error';return}const updates={};selected.forEach(path=>{const [section,key]=path.split('.',2);(updates[section]??={})[key]=bulkOriginal[section][key]});try{button.disabled=true;state.textContent=tr('正在保存并应用…');const d=await get('/api/v1/fleet/config',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${hostToken()}`},body:JSON.stringify({targets,updates})}),failed=d.results.filter(x=>!x.ok);state.textContent=failed.length?(LANG==='zh'?`部分失败:${failed.map(x=>`${x.id}: ${x.error}`).join(';')}`:`Partial failure: ${failed.map(x=>`${x.id}: ${x.error}`).join('; ')}`):(LANG==='zh'?`已同步 ${selected.length} 项配置到 ${targets.length} 台机器`:`Synced ${selected.length} settings to ${targets.length} hosts`);state.className=failed.length?'error':'success'}catch(e){state.textContent=e.message;state.className='error'}finally{button.disabled=false}}
|
||
document.querySelectorAll('[data-log-range]').forEach(b=>b.onclick=()=>{setLogRange(b.dataset.logRange);loadLogs()});document.querySelectorAll('[data-log-sort]').forEach(b=>b.onclick=()=>{const value=b.dataset.logSort;if(logState.sort===value)logState.order=logState.order==='asc'?'desc':'asc';else{logState.sort=value;logState.order=value==='timestamp'?'desc':'asc'}loadLogs()});$('refreshLogs').onclick=()=>loadLogs();$('resetLogs').onclick=()=>{setLogRange('today');$('logSeverity').value='';$('logMachine').value='';$('logEvent').value='';$('logSearch').value='';logState.sort='timestamp';logState.order='desc';loadLogs()};$('logPrev').onclick=()=>{logState.offset=Math.max(0,logState.offset-logState.limit);loadLogs(false)};$('logNext').onclick=()=>{if(logState.offset+logState.limit<logState.total){logState.offset+=logState.limit;loadLogs(false)}};
|
||
const language=$('language'),theme=$('theme'),themeMedia=matchMedia('(prefers-color-scheme: dark)');language.value=LANG;theme.value=localStorage.getItem('pigwayTheme')||'auto';applyTheme(theme.value);language.onchange=()=>{LANG=language.value;localStorage.setItem('pigwayLanguage',LANG);location.reload()};theme.onchange=()=>{localStorage.setItem('pigwayTheme',theme.value);applyTheme(theme.value)};themeMedia.addEventListener?.('change',()=>{if(theme.value==='auto')applyTheme('auto')});translateNode();new MutationObserver(ms=>ms.forEach(m=>m.addedNodes.forEach(n=>{if(n.nodeType===Node.ELEMENT_NODE)translateNode(n);else if(n.nodeType===Node.TEXT_NODE){const map=LANG==='en'?EN:ZH,trim=n.nodeValue.trim();if(map[trim])n.nodeValue=n.nodeValue.replace(trim,map[trim])}}))).observe(document.body,{childList:true,subtree:true});document.documentElement.lang=LANG==='zh'?'zh-CN':'en';setLogRange('today');$('configHost').onchange=()=>{currentConfigHost=$('configHost').value;loadConfig()};$('saveConfig').onclick=applyConfig;$('managerConnect').onclick=async()=>{sessionStorage.setItem('pigwayManagerToken',$('managerToken').value.trim());try{await get('/api/v1/hosts');$('managerLogin').classList.add('hidden');$('managerToken').value='';loadStatus()}catch(e){$('managerLoginState').textContent=e.message}};if(hostToken()){$('managerLogin').classList.add('hidden');loadStatus()}setInterval(()=>{if(hostToken())loadStatus()},3000);
|
||
</script></body></html>
|