Add ZFS drive state (ONLINE/FAULTED/DEGRADED) to UI

This commit is contained in:
2026-03-07 04:44:31 +00:00
parent cea4db53fd
commit a25ce4ae21
5 changed files with 29 additions and 7 deletions

View File

@@ -264,7 +264,7 @@ function TableView({ enclosure, onSelect, selectedSerial, t }) {
<table style={{ width: "100%", borderCollapse: "separate", borderSpacing: "0 4px", fontSize: 13 }}> <table style={{ width: "100%", borderCollapse: "separate", borderSpacing: "0 4px", fontSize: 13 }}>
<thead> <thead>
<tr style={{ textAlign: "left", color: t.textSecondary, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.8 }}> <tr style={{ textAlign: "left", color: t.textSecondary, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.8 }}>
{["Slot", "Device", "Model", "Serial", "WWN", "FW", "Capacity", "Pool", "Vdev", "Temp", "Hours", "Health"].map((h) => ( {["Slot", "Device", "Model", "Serial", "WWN", "FW", "Capacity", "Pool", "Vdev", "ZFS State", "Temp", "Hours", "Health"].map((h) => (
<th key={h} style={{ padding: "6px 10px", fontWeight: 600, borderBottom: `2px solid ${t.divider}` }}>{h}</th> <th key={h} style={{ padding: "6px 10px", fontWeight: 600, borderBottom: `2px solid ${t.divider}` }}>{h}</th>
))} ))}
</tr> </tr>
@@ -314,6 +314,17 @@ function TableView({ enclosure, onSelect, selectedSerial, t }) {
<span style={{ fontSize: 11, color: t.textMuted, fontStyle: "italic" }}>{"\u2014"}</span> <span style={{ fontSize: 11, color: t.textMuted, fontStyle: "italic" }}>{"\u2014"}</span>
)} )}
</td> </td>
<td style={{ padding: "8px 10px" }}>
{d.zfs_state ? (
<span style={{
fontSize: 11, fontWeight: 700,
fontFamily: "'JetBrains Mono', monospace",
color: d.zfs_state === "ONLINE" ? t.health.healthy.text : d.zfs_state === "DEGRADED" ? t.health.warning.text : d.zfs_state === "FAULTED" || d.zfs_state === "UNAVAIL" ? t.health.error.text : t.textSecondary,
}}>{d.zfs_state}</span>
) : (
<span style={{ fontSize: 11, color: t.textMuted, fontStyle: "italic" }}>{"\u2014"}</span>
)}
</td>
<td style={{ padding: "8px 10px", fontWeight: 600, color: d.temperature_c >= 40 ? t.health.warning.text : t.text }}>{d.temperature_c != null ? `${d.temperature_c}\u00B0C` : "\u2014"}</td> <td style={{ padding: "8px 10px", fontWeight: 600, color: d.temperature_c >= 40 ? t.health.warning.text : t.text }}>{d.temperature_c != null ? `${d.temperature_c}\u00B0C` : "\u2014"}</td>
<td style={{ padding: "8px 10px", color: t.text }}>{formatHours(d.power_on_hours)}</td> <td style={{ padding: "8px 10px", color: t.text }}>{formatHours(d.power_on_hours)}</td>
<td style={{ padding: "8px 10px", borderRadius: "0 6px 6px 0" }}><StatusPill status={healthStatus} t={t} /></td> <td style={{ padding: "8px 10px", borderRadius: "0 6px 6px 0" }}><StatusPill status={healthStatus} t={t} /></td>
@@ -431,12 +442,17 @@ function DriveDetail({ slot, onClose, t }) {
/ {d.zfs_vdev} / {d.zfs_vdev}
</span> </span>
)} )}
<span style={{ {d.zfs_state && (
fontSize: 10, fontWeight: 600, color: t.health.healthy.text, opacity: 0.7, <span style={{
background: t.health.healthy.border + "33", padding: "2px 6px", borderRadius: 4, fontSize: 11, fontWeight: 700,
}}> fontFamily: "'JetBrains Mono', monospace",
MEMBER color: d.zfs_state === "ONLINE" ? t.health.healthy.text : d.zfs_state === "DEGRADED" ? t.health.warning.text : t.health.error.text,
</span> background: (d.zfs_state === "ONLINE" ? t.health.healthy.border : d.zfs_state === "DEGRADED" ? t.health.warning.border : t.health.error.border) + "33",
padding: "2px 8px", borderRadius: 4,
}}>
{d.zfs_state}
</span>
)}
</> </>
) : ( ) : (
<> <>

View File

@@ -34,6 +34,7 @@ class DriveDetail(BaseModel):
wear_leveling_percent: int | None = None wear_leveling_percent: int | None = None
zfs_pool: str | None = None zfs_pool: str | None = None
zfs_vdev: str | None = None zfs_vdev: str | None = None
zfs_state: str | None = None
smart_attributes: list[dict] = [] smart_attributes: list[dict] = []
@@ -53,6 +54,7 @@ class DriveHealthSummary(BaseModel):
uncorrectable_errors: int | None = None uncorrectable_errors: int | None = None
zfs_pool: str | None = None zfs_pool: str | None = None
zfs_vdev: str | None = None zfs_vdev: str | None = None
zfs_state: str | None = None
health_status: str = "healthy" health_status: str = "healthy"

View File

@@ -23,5 +23,6 @@ async def get_drive_detail(device: str):
if zfs_info: if zfs_info:
data["zfs_pool"] = zfs_info["pool"] data["zfs_pool"] = zfs_info["pool"]
data["zfs_vdev"] = zfs_info["vdev"] data["zfs_vdev"] = zfs_info["vdev"]
data["zfs_state"] = zfs_info.get("state")
return DriveDetail(**data) return DriveDetail(**data)

View File

@@ -95,6 +95,7 @@ async def get_overview():
uncorrectable_errors=sd.get("uncorrectable_errors"), uncorrectable_errors=sd.get("uncorrectable_errors"),
zfs_pool=pool_map.get(sd["device"], {}).get("pool"), zfs_pool=pool_map.get(sd["device"], {}).get("pool"),
zfs_vdev=pool_map.get(sd["device"], {}).get("vdev"), zfs_vdev=pool_map.get(sd["device"], {}).get("vdev"),
zfs_state=pool_map.get(sd["device"], {}).get("state"),
health_status=health_status, health_status=health_status,
) )
elif s["populated"]: elif s["populated"]:

View File

@@ -71,9 +71,11 @@ async def get_zfs_pool_map() -> dict[str, dict]:
parts = stripped.split() parts = stripped.split()
dev_path = parts[0] dev_path = parts[0]
try: try:
dev_state = parts[1] if len(parts) > 1 else None
info = { info = {
"pool": current_pool, "pool": current_pool,
"vdev": current_vdev or current_pool, "vdev": current_vdev or current_pool,
"state": dev_state,
} }
# Resolve symlink and map the device # Resolve symlink and map the device
real = os.path.realpath(dev_path) real = os.path.realpath(dev_path)