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()