From 849fd016b6f560a235edd9abc9999f4e0e77e3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=A1=E5=8F=A3=E6=B5=AA=E4=BA=BA?= Date: Sun, 27 Sep 2026 20:31:29 +0800 Subject: [PATCH] fix: restore CPU meter and explain unavailable fan status --- SHA256SUMS | 2 +- web/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SHA256SUMS b/SHA256SUMS index 220f776..a5d72a8 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -7,5 +7,5 @@ ffd1790c882ca5dbcaebdfadc8f24c504159900213eebaa22e61fcdbf3facfe0 README.md 3da8b5d94e2206bffc19e1c4fa493b31880c9a90fd6a02546461fe54a7255dbd config/pigway-pi-control.conf 20c584762cae3a50a498df25e11272c3c191a62c9ce823e63f8224d14637b1d4 systemd/pigway-pi-control-api.service 35ec68e1c3040e7affad43961fbb77e9e29f3474b92aed1005502629f9acb549 systemd/pigway-pi-control.service -c953710a48728927c9327ba9f94440d0368233e4bd5562d4deb3b1adf0b51851 web/index.html +95b258d06fdf22a6bd2112ea2208b87be7a61e8eb3c68dcb29fe2b4e5e7dd6a3 web/index.html fa7ab991a7a39cce72b218e407dcefbcede99c8d8eb3d9c161a5296d506c41a9 docs/HARDWARE_PLUGIN_API.md diff --git a/web/index.html b/web/index.html index 43c8c88..fbf76b7 100644 --- a/web/index.html +++ b/web/index.html @@ -58,7 +58,7 @@ function ownerName(id){const [type,suffix]=String(id||'').split('|',2),name=owne 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=``;else{const input=``;control=unit?`
${input}${unit}
`:input}return`
${help?`
${esc(help)}
`:''}
`} function pluginText(zh,en){return LANG==='zh'?zh:en} function renderPlugins(){return `

${pluginText('仅发现插件不会接管灯光。接入后发送监控状态;暂停或断开不会停止本地温控。','Discovery does not take over lighting. Connecting sends monitoring state; pausing or disconnecting leaves autonomous cooling running.')}

`} -function pluginSummary(plugins,host){return Object.entries(plugins).map(([id,p])=>{const st=p.status||{},cap=p.capabilities||{},details=[];if(p.online){if(cap.fan)details.push(pluginText('风扇设置','Fan setting')+' '+esc(st.fan_name||'—'));if(cap.display)details.push('OLED '+esc(ownerName(st.oled_page||'')));if(cap.rgb)details.push('RGB '+esc(rgbName(st.rgb_mode||'')))}return `
${esc(p.name||id)} · ${p.online?tr('在线'):tr('离线')} ${details.join(' · ')}
`}).join('')} +function pluginSummary(plugins,host){if(!Object.keys(plugins).length)return `
${pluginText('风扇与硬件插件','Fan and hardware plugins')}

${pluginText('尚未接入硬件插件,风扇状态不可用。独立安装并启动插件后,可在插件管理中接入。','No hardware plugin connected; fan status is unavailable. Install and start a plugin independently, then connect it in plugin management.')}

`;return Object.entries(plugins).map(([id,p])=>{const st=p.status||{},cap=p.capabilities||{},details=[];if(p.online){if(cap.fan)details.push(pluginText('风扇设置','Fan setting')+' '+esc(st.fan_name||'—'));if(cap.display)details.push('OLED '+esc(ownerName(st.oled_page||'')));if(cap.rgb)details.push('RGB '+esc(rgbName(st.rgb_mode||'')))}return `
${esc(p.name||id)} · ${p.online?tr('在线'):tr('离线')} ${details.join(' · ')}
`}).join('')} let pluginRows=[],pluginDraft=null,pluginSelected=''; async function loadPlugins(){const list=$('pluginList');if(!list)return;list.textContent=pluginText('正在发现本机插件…','Discovering local plugins…');try{const data=await get('/api/v1/plugins?host='+encodeURIComponent(currentConfigHost));pluginRows=data.plugins;list.innerHTML=pluginRows.map(p=>`

${esc(p.descriptor?.name||p.id)}

${esc(p.socket)} · ${p.online?tr('在线'):tr('离线')} · ${p.linked?(p.paused?pluginText('联动已暂停','Link paused'):pluginText('已接入','Connected')):pluginText('尚未接入','Not connected')}
${p.error?`

${esc(p.error)}

`:''}${p.status?.errors&&Object.keys(p.status.errors).length?`

${esc(JSON.stringify(p.status.errors))}

`:''}

${Object.keys(p.descriptor?.capabilities||{}).map(k=>esc(({fan:pluginText('风扇','Fan'),display:pluginText('屏幕','Display'),rgb:pluginText('灯光','Lighting')})[k]||k)).join(' · ')}

${p.linked?``:``}
`).join('')||`

${pluginText('未发现硬件插件。请在目标机器上独立安装并启动所需插件;没有散热板也不影响监控。','No hardware plugins found. Install and start the required plugin on the target host. Monitoring does not require a cooling board.')}

`;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}} @@ -94,7 +94,7 @@ 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=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`
`} -function hostCard(entry){const h=entry.host;if(!entry.online)return`

${esc(h.name||h.id)}

ID ${esc(h.id)}${esc(h.url||tr('本机'))}
${tr('离线')}
${esc(entry.error||tr('连接失败'))}
`;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`

${esc(m.name||h.name||m.id)}

ID ${esc(m.id||h.id)}${LANG==='zh'?'主机名':'Hostname'} ${esc(m.hostname||v.hostname||'--')}${esc(v.model||'--')}${LANG==='zh'?'序列号':'Serial'} ${esc(v.serial||'--')}${LANG==='zh'?'硬件版本':'Revision'} ${esc(v.revision||'--')}${esc(v.os||'--')}${LANG==='zh'?'内核':'Kernel'} ${esc(v.kernel||'--')}${esc(v.architecture||'--')} · CPU ${esc(v.cpu_count||'--')}${LANG==='zh'?'程序版本':'Agent'} ${esc(s.version||'--')}
${tr('在线')}
${tr('处理器负载')}
${esc(y.cpu_percent)}%
${LANG==='zh'?'频率':'Frequency'} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C
${tr('内存占用')}
${esc(y.memory_percent)}%
${meter(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${LANG==='zh'?'可用':'Available'} ${bytes(y.memory_available_bytes)}
${tr('磁盘占用')}
${esc(y.disk_percent)}%
${meter(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${LANG==='zh'?'剩余':'Free'} ${bytes(y.disk_free_bytes)}
${tr('主网络')}
${n.kind==='WIF'&&signal!==null&&signal!==undefined?`${esc(signal)}%`:esc(n.kind||'--')}
${n.kind==='WIF'?meter(signal):''}
${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')}
${tr('显示状态')}
${esc(ownerName(d.owner))}
${LANG==='zh'?'告警状态独立于硬件插件':'Alert state is independent of hardware plugins'}
${tr('当前告警')}
${(d.alerts||[]).length}
${(d.alerts||[]).map(a=>esc(ownerName(a.id))).join(' · ')||tr('无告警')}
${pluginSummary(s.plugins||{},m.id||h.id)}

${tr('服务与进程监控')}

${(s.watches||[]).map(w=>`
${esc(w.name)} · ${esc(w.target)}${tr(w.up?'正常':'停止')}
`).join('')||`
${tr('暂无数据')}
`}
`} +function hostCard(entry){const h=entry.host;if(!entry.online)return`

${esc(h.name||h.id)}

ID ${esc(h.id)}${esc(h.url||tr('本机'))}
${tr('离线')}
${esc(entry.error||tr('连接失败'))}
`;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`

${esc(m.name||h.name||m.id)}

ID ${esc(m.id||h.id)}${LANG==='zh'?'主机名':'Hostname'} ${esc(m.hostname||v.hostname||'--')}${esc(v.model||'--')}${LANG==='zh'?'序列号':'Serial'} ${esc(v.serial||'--')}${LANG==='zh'?'硬件版本':'Revision'} ${esc(v.revision||'--')}${esc(v.os||'--')}${LANG==='zh'?'内核':'Kernel'} ${esc(v.kernel||'--')}${esc(v.architecture||'--')} · CPU ${esc(v.cpu_count||'--')}${LANG==='zh'?'程序版本':'Agent'} ${esc(s.version||'--')}
${tr('在线')}
${tr('处理器负载')}
${esc(y.cpu_percent)}%
${meter(y.cpu_percent)}
${LANG==='zh'?'频率':'Frequency'} ${esc(y.cpu_freq_mhz)} MHz · ${esc(y.temperature_c)}°C
${tr('内存占用')}
${esc(y.memory_percent)}%
${meter(y.memory_percent)}
${bytes(memUsed)} / ${bytes(memTotal)} · ${LANG==='zh'?'可用':'Available'} ${bytes(y.memory_available_bytes)}
${tr('磁盘占用')}
${esc(y.disk_percent)}%
${meter(y.disk_percent)}
${bytes(diskUsed)} / ${bytes(diskTotal)} · ${LANG==='zh'?'剩余':'Free'} ${bytes(y.disk_free_bytes)}
${tr('主网络')}
${n.kind==='WIF'&&signal!==null&&signal!==undefined?`${esc(signal)}%`:esc(n.kind||'--')}
${n.kind==='WIF'?meter(signal):''}
${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')}
${tr('显示状态')}
${esc(ownerName(d.owner))}
${LANG==='zh'?'告警状态独立于硬件插件':'Alert state is independent of hardware plugins'}
${tr('当前告警')}
${(d.alerts||[]).length}
${(d.alerts||[]).map(a=>esc(ownerName(a.id))).join(' · ')||tr('无告警')}
${pluginSummary(s.plugins||{},m.id||h.id)}

${tr('服务与进程监控')}

${(s.watches||[]).map(w=>`
${esc(w.name)} · ${esc(w.target)}${tr(w.up?'正常':'停止')}
`).join('')||`
${tr('暂无数据')}
`}
`} function renderHostCards(entries){const root=$('hostCards'),seen=new Set;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};