fix: polish themes and language consistency
This commit is contained in:
+1
-1
@@ -7,4 +7,4 @@ f9fbb4defa45b43abe0ce66973b1f4d553369b3a43c01d04f206c573f7dc96ca install.sh
|
||||
4f6e8db3698a62f57823d3298a0e6814593de6fa3d31e4751b7ea4a76e553e35 systemd/pigway-pi-control.service
|
||||
aa21cef9a06593371ccff005335b6c0c826fbf4b5c48e4255b5f4b75ce6e9217 systemd/pigway-pi-control-api.service
|
||||
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
||||
8a900e5fbf442f0b7626508edd70f845e62511836363fc70b0d11aed2e1cf774 web/index.html
|
||||
3a40109f11bea2d44e9d61bed4a90499a5047c4fc534f97aaa90becbf9e57017 web/index.html
|
||||
|
||||
+9
-8
@@ -8,7 +8,7 @@
|
||||
</style>
|
||||
<style>
|
||||
.log-filter{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;align-items:end}.log-filter label{color:var(--muted);font-size:12px}.log-filter input,.log-filter 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:wrap;margin:12px 0}.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:800px){.log-filter{grid-template-columns:1fr 1fr}}@media(max-width:470px){.log-filter{grid-template-columns:1fr}}
|
||||
.header-actions{display:flex;align-items:end;gap:12px;flex-wrap:wrap;justify-content:flex-end}.preference{display:grid;gap:4px;color:var(--muted);font-size:12px}.preference select{border:1px solid var(--line);border-radius:7px;background:#0d151e;color:var(--text);padding:7px 28px 7px 9px}.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"] .card,[data-theme="light"] .panel,[data-theme="light"] .group{box-shadow:0 10px 24px #3452}[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 input,[data-theme="light"] .log-filter select,[data-theme="light"] .preference select{background:#fff}[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}.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}.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(max-width:900px){.host-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:550px){.host-grid{grid-template-columns:1fr}.host-head{flex-direction:column}}
|
||||
.header-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:flex-end}.preference{display:grid;gap:4px;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 28px 7px 9px}.header-actions .live{align-self:flex-end;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"] .card,[data-theme="light"] .panel,[data-theme="light"] .group{box-shadow:0 10px 24px #3452}[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 input,[data-theme="light"] .log-filter 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}.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}.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(max-width:900px){.host-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:550px){.host-grid{grid-template-columns:1fr}.host-head{flex-direction:column}}
|
||||
</style>
|
||||
</head>
|
||||
<body><main>
|
||||
@@ -18,7 +18,7 @@
|
||||
<section id="logs" class="hidden"><div class="panel"><h2>事件日志</h2><p class="muted">查询 systemd journal 中仍在保留的全部硬件 Agent 事件。</p><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><div class="log-filter"><label>开始日期<input id="logStart" type="date"></label><label>结束日期<input id="logEnd" type="date"></label><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><label>关键词<input id="logSearch" placeholder="搜索日志内容"></label></div><div class="toolbar"><button id="refreshLogs">查询</button><button id="resetLogs">重置</button><span class="muted" id="logSummary"></span></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="config" class="hidden"><div class="panel"><h2>系统配置</h2><p class="muted">配置按用途分组,只显示当前模式需要的选项。保存时会备份配置并重启硬件 Agent。</p><div class="toolbar"><label class="host-select">配置机器<select id="configHost"></select></label><input id="token" type="password" placeholder="当前机器配置令牌(Bearer Token)"><button id="toggleToken">显示令牌</button></div><div class="group"><div class="group-head"><div><h3>多机配置同步</h3><div class="hint">勾选目标机器后,将当前页面中已修改的配置项批量同步到这些机器。</div></div></div><div id="syncHosts" class="host-checks"></div></div><div class="config-shell"><aside class="config-menu" id="configMenu"></aside><div id="configRows"></div></div><div class="savebar"><button id="saveConfig">保存到当前机器</button> <button id="syncConfig">批量同步已修改项</button> <span id="saveState" class="muted">尚未修改</span></div></div></section>
|
||||
</main><script>
|
||||
const LANG=localStorage.getItem('pigwayLanguage')||'zh';
|
||||
let LANG=localStorage.getItem('pigwayLanguage')||'zh';
|
||||
const EN={
|
||||
'语言':'Language','颜色模式':'Appearance','中文':'中文','自动':'Auto','深色':'Dark','浅色':'Light','正在连接树莓派…':'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',
|
||||
@@ -37,8 +37,9 @@ Object.assign(EN,{
|
||||
'处理器、温度与散热':'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.','硬件校准项,通常保持 10 毫秒。':'Hardware calibration setting; 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'});
|
||||
const tr=s=>LANG==='en'?(EN[s]||s):s;
|
||||
function translateNode(root=document){if(LANG!=='en')return;const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);let n;while(n=walker.nextNode()){const raw=n.nodeValue,trim=raw.trim();if(trim&&EN[trim])n.nodeValue=raw.replace(trim,EN[trim])}root.querySelectorAll?.('[placeholder],[title]').forEach(e=>['placeholder','title'].forEach(a=>{const v=e.getAttribute(a);if(v&&EN[v])e.setAttribute(a,EN[v])}))}
|
||||
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'});
|
||||
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]').forEach(e=>['placeholder','title'].forEach(a=>{const v=e.getAttribute(a);if(v)e.setAttribute(a,translate(v))}))}
|
||||
function applyTheme(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={device:['设备标识','自定义本机显示名称和稳定标识'],hosts:['机器列表','登记需要聚合监控的其他节点'],led:['灯光设置','正常灯效与告警颜色'],alerts:['告警阈值','处理器、内存、温度、磁盘和 Wi-Fi'],fan:['风扇温控','温度档位与降档回差'],timing:['刷新周期','传感器、屏幕、网络和健康检查'],display:['屏幕布局','OLED 四行文字位置'],dark_mode:['板载指示灯','树莓派电源灯与活动灯'],services:['服务监控','监控 systemd 服务'],processes:['进程监控','监控普通进程关键字'],api:['Web 与 API','监听地址、端口和日志数量']};
|
||||
@@ -74,13 +75,13 @@ async function monitorAction(unit,action){const s=serviceCache.find(x=>x.unit===
|
||||
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({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||'本机')}</span></div></div><span class="badge bad">${tr('离线')}</span></div><div class="error">${esc(entry.error||'连接失败')}</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>Hostname ${esc(m.hostname||v.hostname||'--')}</span><span>${esc(v.model||'--')}</span><span>Serial ${esc(v.serial||'--')}</span><span>Revision ${esc(v.revision||'--')}</span><span>${esc(v.os||'--')}</span><span>Kernel ${esc(v.kernel||'--')}</span><span>${esc(v.architecture||'--')} · CPU ${esc(v.cpu_count||'--')}</span><span>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><div class="muted">${LANG==='zh'?'频率':'Frequency'} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C · ${LANG==='zh'?'风扇':'Fan'} ${esc(y.fan_name)}</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">OLED: ${esc(ownerName(d.oled_page))} · RGB: ${esc(rgbName(d.rgb_mode))}</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><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 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.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><div class="muted">${LANG==='zh'?'频率':'Frequency'} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C · ${LANG==='zh'?'风扇':'Fan'} ${esc(y.fan_name)}</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">OLED: ${esc(ownerName(d.oled_page))} · RGB: ${esc(rgbName(d.rgb_mode))}</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><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>`}
|
||||
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()}`;$('hostCards').innerHTML=fleet.hosts.map(hostCard).join('');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`;$('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 name=b.textContent.replace(/[ ↑↓]$/,'');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>`}}
|
||||
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`;$('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(id){return localStorage.getItem(`pigwayToken:${id}`)||localStorage.getItem('pigwayToken')||''}
|
||||
function saveHostToken(id,value){localStorage.setItem(`pigwayToken:${id}`,value);if(fleetHosts.find(x=>x.id===id)?.local)localStorage.setItem('pigwayToken',value)}
|
||||
function refreshHostSelectors(){if(!fleetHosts.length)return;const current=$('configHost').value||currentConfigHost||fleetHosts[0].id,checked=new Set([...document.querySelectorAll('#syncHosts input:checked')].map(x=>x.value));$('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;$('syncHosts').innerHTML=fleetHosts.map(h=>`<label><input type="checkbox" value="${esc(h.id)}" ${checked.has(h.id)?'checked':''}>${esc(h.name||h.id)}</label>`).join('')}
|
||||
@@ -88,8 +89,8 @@ function changedUpdates(){const updates={};document.querySelectorAll('#configRow
|
||||
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;$('token').value=hostToken(currentConfigHost);const d=await get(`/api/v1/config?host=${encodeURIComponent(currentConfigHost)}`);original=d.config;renderConfig();markChanged()}catch(e){$('configRows').innerHTML=`<div class="error">${esc(e.message)}</div>`}}
|
||||
async function saveHost(){const id=$('hostId').value.trim(),url=$('hostUrl').value.trim(),message=$('hostMessage');try{message.textContent='正在保存…';const local=fleetHosts.find(x=>x.local),token=hostToken(local?.id||'');const d=await get('/api/v1/hosts',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({operation:'save',id,url})});fleetHosts=d.hosts;refreshHostSelectors();await loadConfig();message.textContent='保存成功'}catch(e){message.textContent=e.message;message.className='error'}}
|
||||
async function deleteHost(id){if(!confirm(`确定删除机器 ${id} 吗?`))return;try{const local=fleetHosts.find(x=>x.local),token=hostToken(local?.id||'');const d=await get('/api/v1/hosts',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({operation:'delete',id})});fleetHosts=d.hosts;currentConfigHost=local?.id||'';refreshHostSelectors();await loadConfig()}catch(e){alert(e.message)}}
|
||||
async function deleteHost(id){if(!confirm(LANG==='zh'?`确定删除机器 ${id} 吗?`:`Delete host ${id}?`))return;try{const local=fleetHosts.find(x=>x.local),token=hostToken(local?.id||'');const d=await get('/api/v1/hosts',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${token}`},body:JSON.stringify({operation:'delete',id})});fleetHosts=d.hosts;currentConfigHost=local?.id||'';refreshHostSelectors();await loadConfig()}catch(e){alert(e.message)}}
|
||||
async function applyConfig(targets){const updates=changedUpdates();if(targets.length>1){delete updates.device;delete updates.hosts;}if(!Object.keys(updates).length){$('saveState').textContent=tr('没有需要保存的修改');return}const local=fleetHosts.find(x=>x.local),tokens=Object.fromEntries(targets.map(id=>[id,hostToken(id)]));try{$('saveConfig').disabled=true;$('syncConfig').disabled=true;$('saveState').textContent=tr('正在保存并应用…');saveHostToken(currentConfigHost,$('token').value.trim());tokens[currentConfigHost]=$('token').value.trim();const d=await get('/api/v1/fleet/config',{method:'PUT',headers:{'Content-Type':'application/json','Authorization':`Bearer ${hostToken(local?.id||'')}`},body:JSON.stringify({targets,updates,tokens})});const failed=d.results.filter(x=>!x.ok);$('saveState').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'?`已同步到 ${d.results.length} 台机器`:`Synced to ${d.results.length} hosts`);$('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;$('syncConfig').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=()=>{localStorage.setItem('pigwayLanguage',language.value);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&&EN[n.nodeValue.trim()])n.nodeValue=n.nodeValue.replace(n.nodeValue.trim(),EN[n.nodeValue.trim()])}))).observe(document.body,{childList:true,subtree:true});document.documentElement.lang=LANG==='zh'?'zh-CN':'en';setLogRange('today');$('toggleToken').onclick=()=>{const i=$('token'),show=i.type==='password';i.type=show?'text':'password';$('toggleToken').textContent=tr(show?'隐藏令牌':'显示令牌')};$('token').onchange=()=>saveHostToken(currentConfigHost,$('token').value.trim());$('configHost').onchange=()=>{currentConfigHost=$('configHost').value;$('token').value=hostToken(currentConfigHost);loadConfig()};$('saveConfig').onclick=()=>applyConfig([currentConfigHost]);$('syncConfig').onclick=()=>{const targets=[...document.querySelectorAll('#syncHosts input:checked')].map(x=>x.value);if(!targets.length){$('saveState').textContent=LANG==='zh'?'请先选择同步目标机器':'Select target hosts first';return}applyConfig(targets)};loadStatus();setInterval(loadStatus,3000);
|
||||
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');$('toggleToken').onclick=()=>{const i=$('token'),show=i.type==='password';i.type=show?'text':'password';$('toggleToken').textContent=tr(show?'隐藏令牌':'显示令牌')};$('token').onchange=()=>saveHostToken(currentConfigHost,$('token').value.trim());$('configHost').onchange=()=>{currentConfigHost=$('configHost').value;$('token').value=hostToken(currentConfigHost);loadConfig()};$('saveConfig').onclick=()=>applyConfig([currentConfigHost]);$('syncConfig').onclick=()=>{const targets=[...document.querySelectorAll('#syncHosts input:checked')].map(x=>x.value);if(!targets.length){$('saveState').textContent=LANG==='zh'?'请先选择同步目标机器':'Select target hosts first';return}applyConfig(targets)};loadStatus();setInterval(loadStatus,3000);
|
||||
</script></body></html>
|
||||
|
||||
Reference in New Issue
Block a user