feat: 磁盘分区列 — 多分区/进度条/overflow/过滤虚拟FS

SystemInfoCollector:
- _get_partitions(): psutil disk_partitions + 过滤虚拟FS
- 跳过 tmpfs/devtmpfs/proc/sysfs/cgroup等22种
- 无psutil时 fallback到根分区

前端4列: 160px 1fr 0.8fr 1fr
- col-disk: 分区名/挂载点/进度条/百分比
- max-height:200px overflow:auto
- 跨平台兼容

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-14 09:31:52 +08:00
parent 095f6c765c
commit 528410d3e9
2 changed files with 80 additions and 4 deletions
+32 -4
View File
@@ -56,13 +56,22 @@
.node-notes { font-size:.75rem; color:var(--text-dim); font-style:italic; margin-bottom:8px } .node-notes { font-size:.75rem; color:var(--text-dim); font-style:italic; margin-bottom:8px }
/* 横版布局 — 左设备信息 | 右数据+图表同行 */ /* 横版布局 — 左设备信息 | 右数据+图表同行 */
#sentinel-grid.layout-row { display:flex;flex-direction:column;gap:12px } #sentinel-grid.layout-row { display:flex;flex-direction:column;gap:12px }
#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 0.8fr 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:6px;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 { 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 .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 .extra-row .eval { font-weight:500 }
#sentinel-grid.layout-row .col-disk { overflow-y:auto;max-height:200px;padding:0 4px }
#sentinel-grid.layout-row .disk-list { display:flex;flex-direction:column;gap:4px }
#sentinel-grid.layout-row .part-row { display:flex;flex-direction:column;gap:2px;padding:3px 0;border-bottom:1px solid var(--md-sys-color-surface-container-lowest) }
#sentinel-grid.layout-row .part-info { display:flex;justify-content:space-between;font-size:.7rem }
#sentinel-grid.layout-row .part-name { font-weight:500;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap }
#sentinel-grid.layout-row .part-mount { color:var(--text-dim);font-size:.65rem }
#sentinel-grid.layout-row .part-bar { height:3px;border-radius:1px;background:var(--md-sys-color-surface-container-lowest);margin:2px 0 }
#sentinel-grid.layout-row .part-bar-fill { height:100%;border-radius:1px;background:var(--primary);transition:width .6s }
#sentinel-grid.layout-row .part-pct { font-size:.6rem;color:var(--text-dim);text-align:right }
#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 }
@@ -201,11 +210,11 @@
'<div class="col-extra">'+ '<div class="col-extra">'+
'<div class="extra-row"><span class="elabel">负载</span> <span class="eval load-val">—</span></div>'+ '<div class="extra-row"><span class="elabel">负载</span> <span class="eval load-val">—</span></div>'+
'<div class="extra-row"><span class="elabel">SWAP</span> <span class="eval swap-val">—</span></div>'+ '<div class="extra-row"><span class="elabel">SWAP</span> <span class="eval swap-val">—</span></div>'+
'<div class="extra-row"><span class="elabel">磁盘</span> <span class="eval disk-val">—</span></div>'+
'<div class="extra-row"><span class="elabel">温度</span> <span class="eval temp-val">—</span></div>'+ '<div class="extra-row"><span class="elabel">温度</span> <span class="eval temp-val">—</span></div>'+
'<div class="extra-row"><span class="elabel">流量</span> <span class="eval traf-val">—</span></div>'+ '<div class="extra-row"><span class="elabel">流量</span> <span class="eval traf-val">—</span></div>'+
'<div class="extra-row"><span class="elabel">进程</span> <span class="eval proc-val">—</span></div>'+ '<div class="extra-row"><span class="elabel">进程</span> <span class="eval proc-val">—</span></div>'+
'</div>'; '</div>'+
'<div class="col-disk"><div class="disk-list"></div></div>';
} else { } else {
card.innerHTML = card.innerHTML =
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+ '<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
@@ -285,7 +294,26 @@
var sysData = n.system || {}; 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 (loadEl && sysData.cpu) loadEl.textContent = (sysData.cpu.load_avg||[0,0,0]).map(function(v){return v.toFixed(1);}).join(' / ');
if (swapEl && sysData.swap) { var st = sysData.swap.total_gb||0; swapEl.textContent = st > 0 ? (sysData.swap.percent||0).toFixed(1)+'% ('+(sysData.swap.used_gb||0).toFixed(1)+'G)' : '—'; } if (swapEl && sysData.swap) { var st = sysData.swap.total_gb||0; swapEl.textContent = st > 0 ? (sysData.swap.percent||0).toFixed(1)+'% ('+(sysData.swap.used_gb||0).toFixed(1)+'G)' : '—'; }
if (diskEl && sysData.disk) { var dt = sysData.disk.total_gb||0; diskEl.textContent = dt > 0 ? (sysData.disk.percent||0).toFixed(1)+'% ('+(sysData.disk.used_gb||0).toFixed(1)+'/'+(sysData.disk.total_gb||0).toFixed(1)+'G)' : '—'; } // 磁盘分区列表
var diskList = card.querySelector('.disk-list');
if (diskList && sysData.partitions) {
var parts = sysData.partitions || [];
if (!diskList.querySelector('.part-row') || diskList.children.length !== parts.length) {
diskList.innerHTML = parts.map(function(p){
return '<div class="part-row"><div class="part-info"><span class="part-name">'+esc(p.device||'?')+'</span> <span class="part-mount">'+esc(p.mount||'')+'</span></div><div class="part-bar"><div class="part-bar-fill" style="width:'+(p.percent||0)+'%"></div></div><span class="part-pct">'+(p.percent||0).toFixed(0)+'%</span></div>';
}).join('');
} else {
var rows = diskList.querySelectorAll('.part-row');
parts.forEach(function(p, i){
if (rows[i]) {
var fill = rows[i].querySelector('.part-bar-fill');
var pct = rows[i].querySelector('.part-pct');
if (fill) fill.style.width = (p.percent||0)+'%';
if (pct) pct.textContent = (p.percent||0).toFixed(0)+'%';
}
});
}
}
if (tempEl && sysData.temperature) tempEl.textContent = (sysData.temperature.current||0) > 0 ? (sysData.temperature.current).toFixed(1)+'°C' : '—'; 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 (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'; if (procEl && sysData.process) procEl.textContent = (sysData.process.memory_mb||0).toFixed(0)+'MB';
+48
View File
@@ -66,6 +66,7 @@ class SystemInfoCollector:
"memory": self._get_memory(), "memory": self._get_memory(),
"swap": self._get_swap(), "swap": self._get_swap(),
"disk": self._get_disk(), "disk": self._get_disk(),
"partitions": self._get_partitions(),
"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(),
@@ -137,6 +138,53 @@ class SystemInfoCollector:
pass pass
return {"total_gb": 0, "used_gb": 0, "percent": 0} return {"total_gb": 0, "used_gb": 0, "percent": 0}
_SKIP_FS = {'tmpfs', 'devtmpfs', 'devfs', 'overlay', 'squashfs', 'proc', 'sysfs',
'cgroup', 'cgroup2', 'debugfs', 'tracefs', 'fusectl', 'configfs',
'securityfs', 'pstore', 'efivarfs', 'autofs', 'ramfs', 'hugetlbfs',
'mqueue', 'bpf', 'binfmt_misc', 'rpc_pipefs', 'nfsd', 'smb3fs_ctl',
'snapfuse', 'fuse.gvfsd-fuse', 'fuse.portal'}
def _get_partitions(self) -> list:
"""获取存储分区列表 (过滤虚拟文件系统)"""
parts = []
if self.psutil:
try:
for p in self.psutil.disk_partitions(all=False):
if p.fstype and p.fstype.lower() in self._SKIP_FS:
continue
try:
usage = self.psutil.disk_usage(p.mountpoint)
parts.append({
"device": p.device,
"mount": p.mountpoint,
"fstype": p.fstype or "",
"total_gb": round(usage.total / 1073741824, 1),
"used_gb": round(usage.used / 1073741824, 1),
"percent": usage.percent,
})
except (PermissionError, OSError):
parts.append({
"device": p.device,
"mount": p.mountpoint,
"fstype": p.fstype or "",
"total_gb": 0, "used_gb": 0, "percent": 0,
})
except Exception:
pass
if not parts:
# Fallback: just root partition
disk = self._get_disk()
if disk.get("total_gb", 0) > 0:
parts.append({
"device": "/",
"mount": "/",
"fstype": "",
"total_gb": disk["total_gb"],
"used_gb": disk["used_gb"],
"percent": disk["percent"],
})
return parts
def _get_disk(self) -> Dict[str, Any]: def _get_disk(self) -> Dict[str, Any]:
if self.psutil: if self.psutil:
try: try: