diff --git a/plugins/sentinel/dashboard.html b/plugins/sentinel/dashboard.html index 298bbb0..d7bd24f 100644 --- a/plugins/sentinel/dashboard.html +++ b/plugins/sentinel/dashboard.html @@ -59,7 +59,10 @@ #sentinel-grid.layout-row .node-card { display:grid;grid-template-columns:160px 1fr 1fr;gap:12px 16px;align-items:start } #sentinel-grid.layout-row .col-left { min-width:0 } #sentinel-grid.layout-row .col-right-stack { display:flex;flex-direction:column;gap:12px;min-width:0 } - #sentinel-grid.layout-row .col-extra { display:flex;flex-direction:column;gap:8px;padding:0 8px } + #sentinel-grid.layout-row .col-extra { display:flex;flex-direction:column;gap:6px;padding:0 8px } + #sentinel-grid.layout-row .extra-row { font-size:.8rem;padding:3px 0;border-bottom:1px solid var(--md-sys-color-surface-container-lowest) } + #sentinel-grid.layout-row .extra-row .elabel { font-size:.65rem;color:var(--text-dim);text-transform:uppercase;margin-right:4px } + #sentinel-grid.layout-row .extra-row .eval { font-weight:500 } #sentinel-grid.layout-row .mtr-data .rlabel { font-size:.7rem;color:var(--text-dim);text-transform:uppercase;margin-right:6px } #sentinel-grid.layout-row .mtr-data .rval { font-weight:600 } #sentinel-grid.layout-row .mtr { display:flex;align-items:center;height:48px;padding:0 8px;margin-bottom:6px;position:relative } @@ -264,6 +267,21 @@ } } catch(e) {} } + // 更新额外指标 + var loadEl = card.querySelector('.load-val'); + var swapEl = card.querySelector('.swap-val'); + var diskEl = card.querySelector('.disk-val'); + var tempEl = card.querySelector('.temp-val'); + var trafEl = card.querySelector('.traf-val'); + var procEl = card.querySelector('.proc-val'); + var sysData = n.system || {}; + if (loadEl && sysData.cpu) loadEl.textContent = (sysData.cpu.load_avg||[0,0,0]).map(function(v){return v.toFixed(1);}).join(' / '); + if (swapEl && sysData.swap) swapEl.textContent = (sysData.swap.percent||0).toFixed(1)+'% ('+(sysData.swap.used_gb||0).toFixed(1)+'G)'; + if (diskEl && sysData.disk) diskEl.textContent = (sysData.disk.percent||0).toFixed(1)+'% ('+(sysData.disk.used_gb||0).toFixed(1)+'/'+(sysData.disk.total_gb||0).toFixed(1)+'G)'; + if (tempEl && sysData.temperature) tempEl.textContent = (sysData.temperature.current||0) > 0 ? (sysData.temperature.current).toFixed(1)+'°C' : '—'; + if (trafEl && sysData.network) trafEl.textContent = '↓'+(sysData.network.rx_mb||0).toFixed(0)+'M ↑'+(sysData.network.tx_mb||0).toFixed(0)+'M'; + if (procEl && sysData.process) procEl.textContent = (sysData.process.memory_mb||0).toFixed(0)+'MB'; + var infoR = card.querySelector('.col-info'); if (infoR) infoR.textContent = (n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—'); } else { diff --git a/services/web_panel/utils/system_info.py b/services/web_panel/utils/system_info.py index 29c5032..f80336c 100644 --- a/services/web_panel/utils/system_info.py +++ b/services/web_panel/utils/system_info.py @@ -64,9 +64,12 @@ class SystemInfoCollector: }, "cpu": self._get_cpu(), "memory": self._get_memory(), + "swap": self._get_swap(), + "disk": self._get_disk(), "process": self._get_process(), "network": self._get_network(), - "net_speed": self._get_network_speed() + "net_speed": self._get_network_speed(), + "temperature": self._get_temperature(), } def _get_cpu(self) -> Dict[str, Any]: @@ -107,6 +110,71 @@ class SystemInfoCollector: logger.warning(f"Memory info failed: {e}") return {"total_gb": 0, "used_gb": 0, "percent": 0} + def _get_swap(self) -> Dict[str, Any]: + if self.psutil: + try: + s = self.psutil.swap_memory() + return {"total_gb": round(s.total / 1073741824, 1), + "used_gb": round(s.used / 1073741824, 1), + "percent": s.percent} + except Exception: + pass + try: + with open('/proc/meminfo') as f: + mem = {} + for line in f: + parts = line.split() + if len(parts) >= 2: + mem[parts[0].rstrip(':')] = int(parts[1]) * 1024 + t = mem.get('SwapTotal', 0) + f_swap = mem.get('SwapFree', mem.get('SwapCached', 0)) + if t > 0: + used = t - f_swap + return {"total_gb": round(t / 1073741824, 1), + "used_gb": round(used / 1073741824, 1), + "percent": round((used / t) * 100, 1)} + except Exception: + pass + return {"total_gb": 0, "used_gb": 0, "percent": 0} + + def _get_disk(self) -> Dict[str, Any]: + if self.psutil: + try: + d = self.psutil.disk_usage('/') + return {"total_gb": round(d.total / 1073741824, 1), + "used_gb": round(d.used / 1073741824, 1), + "percent": d.percent} + except Exception: + pass + return {"total_gb": 0, "used_gb": 0, "percent": 0} + + def _get_temperature(self) -> Dict[str, Any]: + if self.psutil: + try: + temps = self.psutil.sensors_temperatures() + if temps: + for name, entries in temps.items(): + for e in entries: + if e.current > 0: + return {"name": e.label or name, "current": round(e.current, 1)} + except Exception: + pass + # Android fallback: read thermal zone + try: + for i in range(10): + tz = f'/sys/class/thermal/thermal_zone{i}/temp' + ttype = f'/sys/class/thermal/thermal_zone{i}/type' + if os.path.exists(tz) and os.path.exists(ttype): + with open(ttype) as f: + name = f.read().strip() + if 'cpu' in name.lower() or 'soc' in name.lower(): + with open(tz) as f: + temp = int(f.read().strip()) / 1000.0 + return {"name": name, "current": round(temp, 1)} + except Exception: + pass + return {"name": "", "current": 0} + def _get_process(self) -> Dict[str, Any]: """Current process info (PID + RSS memory)""" pid = os.getpid()