feat: 横版col-extra — 负载/SWAP/磁盘/温度/流量/进程 6项指标

SystemInfoCollector 新增:
- swap: SWAP用量 (psutil + /proc/meminfo fallback)
- disk: 磁盘用量
- temperature: CPU温度 (psutil + /sys/class/thermal fallback)

Sentinel col-extra 显示:
  负载 | SWAP | 磁盘 | 温度 | 累计流量 | 进程RSS

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-14 09:19:37 +08:00
parent 800142402a
commit b1b7cd0563
2 changed files with 88 additions and 2 deletions
+19 -1
View File
@@ -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 .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-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-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 .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-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 } #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) {} } 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'); var infoR = card.querySelector('.col-info');
if (infoR) infoR.textContent = (n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—'); if (infoR) infoR.textContent = (n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—');
} else { } else {
+69 -1
View File
@@ -64,9 +64,12 @@ class SystemInfoCollector:
}, },
"cpu": self._get_cpu(), "cpu": self._get_cpu(),
"memory": self._get_memory(), "memory": self._get_memory(),
"swap": self._get_swap(),
"disk": self._get_disk(),
"process": self._get_process(), "process": self._get_process(),
"network": self._get_network(), "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]: def _get_cpu(self) -> Dict[str, Any]:
@@ -107,6 +110,71 @@ class SystemInfoCollector:
logger.warning(f"Memory info failed: {e}") logger.warning(f"Memory info failed: {e}")
return {"total_gb": 0, "used_gb": 0, "percent": 0} 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]: def _get_process(self) -> Dict[str, Any]:
"""Current process info (PID + RSS memory)""" """Current process info (PID + RSS memory)"""
pid = os.getpid() pid = os.getpid()