diff --git a/AppImage/ProxMenux-1.2.2.2-beta.AppImage b/AppImage/ProxMenux-1.2.2.2-beta.AppImage index 4be06157..82c0867e 100755 Binary files a/AppImage/ProxMenux-1.2.2.2-beta.AppImage and b/AppImage/ProxMenux-1.2.2.2-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 0335f8e8..34594f58 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -79be8caa3baebc9276e409c1ef1e228e33265279df41a03eba8bcc859f67a44d ProxMenux-1.2.2.2-beta.AppImage +fd5f754a35e9657ff4868dba0d5e22c0a9538794e5c176137c33e87f9554c587 ProxMenux-1.2.2.2-beta.AppImage diff --git a/AppImage/components/disk-temperature-card.tsx b/AppImage/components/disk-temperature-card.tsx index b892f0cb..6210afee 100644 --- a/AppImage/components/disk-temperature-card.tsx +++ b/AppImage/components/disk-temperature-card.tsx @@ -64,8 +64,11 @@ export function DiskTemperatureCard({ const fetchHistory = async () => { setLoading(true) try { + // 24-h timeframe gives a more useful "is this drive trending + // up over a day" view; the 1-h window was too short to spot + // anything that mattered. const result = await fetchApi<{ data: TempPoint[] }>( - `/api/disk/${encodeURIComponent(diskName)}/temperature/history?timeframe=hour`, + `/api/disk/${encodeURIComponent(diskName)}/temperature/history?timeframe=day`, ) if (cancelled.current) return setData(result?.data || []) diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index 5a8d6b9c..c363b72f 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -284,6 +284,41 @@ export function StorageOverview() { } } + // Tiny coloured dot that prefixes status / counter values. Adds + // accessibility-friendly redundancy (colour + position) to fields + // that today rely on colour alone, so an "all OK" disk reads as + // visually quiet and a degraded one as immediately noisy. + // + // Colour mapping: + // - "ok" → green (passed / 0 errors) + // - "warn" → amber (1+ errors but not critical) + // - "fail" → red (failed / many errors) + const StatusDot = ({ tone }: { tone: "ok" | "warn" | "fail" }) => { + const cls = + tone === "ok" ? "bg-green-500" : tone === "warn" ? "bg-yellow-500" : "bg-red-500" + return ( + + ) + } + // Decide the tone for a counter where 0 is healthy. The "warn" / + // "fail" cutoffs are conservative — even a single reallocated + // sector is worth amber attention, and double digits start hinting + // at progressive failure (red). + const counterTone = (n: number | null | undefined): "ok" | "warn" | "fail" => { + if (!n || n <= 0) return "ok" + if (n < 10) return "warn" + return "fail" + } + const smartStatusTone = (s: string | undefined): "ok" | "warn" | "fail" => { + const v = (s || "").toLowerCase() + if (v === "passed" || v === "ok") return "ok" + if (v === "failed") return "fail" + return "warn" + } + // Renders either the live temperature or a "Standby" badge for a // spun-down drive. Centralised here because the same pattern shows up // in 4 different disk-list views (system / data / pool / other) and we @@ -313,6 +348,237 @@ export function StorageOverview() { return null } + // ────────────────────────────────────────────────────────────────── + // Disk card layout (ghosthvj-style) + // ────────────────────────────────────────────────────────────────── + // Single card per disk, grid-arranged at the parent level. Two-line + // header (identity + status / size + temp), separator, vertical + // key→value stat list with WEAR LEVEL bar when available, and a + // footer with serial + "Ver detalles →". Replaces the previous + // duplicated mobile/desktop full-width rows. + const renderDiskCardV2 = (disk: DiskInfo) => { + const type = getDiskTypeBadge(disk.name, disk.rotation_rate) + // Pick the most relevant wear metric for the device class. + // NVMe uses `percentage_used` (0 = fresh, 100 = TBW spent). + // SSD may expose `media_wearout_indicator` (decreasing 100→0) or + // `ssd_life_left` (decreasing 100→0). Normalise both to a + // "percentage spent" so the bar always fills LEFT to RIGHT as the + // drive ages — visually consistent across vendors. + let wearPct: number | null = null + if (typeof disk.percentage_used === "number") wearPct = disk.percentage_used + else if (typeof disk.ssd_life_left === "number") wearPct = 100 - disk.ssd_life_left + else if (typeof disk.media_wearout_indicator === "number") + wearPct = 100 - disk.media_wearout_indicator + // Wear bar always uses the same blue as the modal's wear visual, + // even when the wear is high — the colour is the SECTION colour, + // not a severity signal. The percentage value itself (and the + // surrounding stats) already communicate health via the dot + // colours, so flipping the bar to amber/red here would just + // double-encode the same thing and break visual consistency + // with the detail modal. + const wearColor = wearPct === null ? "" : "bg-blue-500" + const cleanSerial = (disk.serial || "").replace(/\\x[0-9a-fA-F]{2}/g, "") + + return ( +
{disk.model}
- )} -Size
-{disk.size_formatted}
-SMART Status
-{disk.smart_status}
-Power On Time
-{formatHours(disk.power_on_hours)}
-Serial
-{disk.serial}
-{disk.model}
- )} -{disk.io_errors.reason}
- )} - > - ) : ( - <> - {disk.io_errors.count} I/O error{disk.io_errors.count !== 1 ? 's' : ''} in 5 min - {disk.io_errors.sample && ( -{disk.io_errors.sample}
- )} - > - )} -Size
-{disk.size_formatted}
-SMART Status
-{disk.smart_status}
-Power On Time
-{formatHours(disk.power_on_hours)}
-Serial
-{disk.serial.replace(/\\x[0-9a-fA-F]{2}/g, '')}
-{disk.model}
- )} -Size
-{disk.size_formatted}
-SMART Status
-{disk.smart_status}
-Serial
-{disk.serial.replace(/\\x[0-9a-fA-F]{2}/g, '')}
-{disk.model}
- )} - - {disk.io_errors && disk.io_errors.count > 0 && ( -{disk.io_errors.reason}
- )} - > - ) : ( - <> - {disk.io_errors.count} I/O error{disk.io_errors.count !== 1 ? 's' : ''} in 5 min - {disk.io_errors.sample && ( -{disk.io_errors.sample}
- )} - > - )} -Size
-{disk.size_formatted}
-SMART Status
-{disk.smart_status}
-Power On Time
-{formatHours(disk.power_on_hours)}
-Serial
-{disk.serial.replace(/\\x[0-9a-fA-F]{2}/g, '')}
-SMART Status
-{selectedDisk.smart_status}
+
+
Reallocated Sectors
-0 ? "text-yellow-500" : ""}`} - > +
+
Pending Sectors
-0 ? "text-yellow-500" : ""}`} - > +
+
CRC Errors
-0 ? "text-yellow-500" : ""}`} - > +
+