diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ea16bfe..56bf2a0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -264,7 +264,7 @@ function TableView({ enclosure, onSelect, selectedSerial, t }) { - {["Slot", "Device", "Model", "Serial", "WWN", "FW", "Capacity", "Pool", "Temp", "Hours", "Health"].map((h) => ( + {["Slot", "Device", "Model", "Serial", "WWN", "FW", "Capacity", "Pool", "Vdev", "Temp", "Hours", "Health"].map((h) => ( ))} @@ -304,6 +304,16 @@ function TableView({ enclosure, onSelect, selectedSerial, t }) { {"\u2014"} )} + @@ -413,6 +423,14 @@ function DriveDetail({ slot, onClose, t }) { }}> {d.zfs_pool} + {d.zfs_vdev && ( + + / {d.zfs_vdev} + + )} dict[str, str]: - """Return a dict mapping device names to ZFS pool names. +async def get_zfs_pool_map() -> dict[str, dict]: + """Return a dict mapping device names to ZFS pool and vdev info. - e.g. {"sda": "tank", "sdb": "tank", "sdc": "fast"} + e.g. {"sda": {"pool": "tank", "vdev": "raidz2-0"}, + "sdb": {"pool": "fast", "vdev": "mirror-0"}} """ pool_map = {} try: @@ -33,19 +35,49 @@ async def get_zfs_pool_map() -> dict[str, str]: return pool_map current_pool = None + current_vdev = None + in_config = False + for line in stdout.decode().splitlines(): stripped = line.strip() + if stripped.startswith("pool:"): current_pool = stripped.split(":", 1)[1].strip() - elif current_pool and "/dev/" in stripped: + current_vdev = None + in_config = False + continue + + if stripped.startswith("NAME") and "STATE" in stripped: + in_config = True + continue + + if stripped.startswith("errors:") or stripped == "": + if stripped.startswith("errors:"): + in_config = False + continue + + if not in_config or not current_pool: + continue + + # Count leading tab depth to distinguish pool/vdev/device lines. + # Pool name = 1 tab, vdev = 2 tabs, device = 3+ tabs + tab_count = len(line) - len(line.lstrip('\t')) + + if tab_count == 2 and "/dev/" not in stripped: + # This is a vdev line (mirror-0, raidz2-0, etc.) + current_vdev = stripped.split()[0] + elif "/dev/" in stripped: parts = stripped.split() dev_path = parts[0] try: real = os.path.realpath(dev_path) dev_name = os.path.basename(real) # Strip partition numbers (sda1 -> sda) - dev_name = dev_name.rstrip("0123456789") - pool_map[dev_name] = current_pool + dev_name = re.sub(r'\d+$', '', dev_name) + pool_map[dev_name] = { + "pool": current_pool, + "vdev": current_vdev or current_pool, + } except Exception: pass except FileNotFoundError:
{h}
+ {d.zfs_vdev ? ( + {d.zfs_vdev} + ) : ( + {"\u2014"} + )} + = 40 ? t.health.warning.text : t.text }}>{d.temperature_c != null ? `${d.temperature_c}\u00B0C` : "\u2014"} {formatHours(d.power_on_hours)}