From d6ccdd6943656cd5ca2c85d3277b64d86334eb66 Mon Sep 17 00:00:00 2001
From: rcourtman <8825017+rcourtman@users.noreply.github.com>
Date: Mon, 5 Oct 2026 06:37:24 +0100
Subject: [PATCH 1/4] Say why TrueNAS storage is unhealthy, drop the healthy
pills
The Storage tab put a green Healthy pill on every pool, dataset and disk,
repeating the status dot, while the few amber rows gave no reason. The
pool said Warning where the dataset said Attention for the same bucket,
and in the disk scope the pill clipped past the phone edge.
Healthy rows now leave Health empty. A non-healthy row says why in the
Health column, on the row's single line as the shared platform-table
rhythm requires: TrueNAS's own alert text first, Pulse's risk summary when
TrueNAS raised no alert, then the dataset state tag (read-only datasets
raise no incident, so the tag is the provider's only reason), and a plain
fallback so no amber row is silent. Stale TrueNAS data leads when it is
the cause. A count stands in for further reasons, with every reason on
hover and for screen readers. Health sorts by severity and is
desktop-only in every scope, so phones keep the status dot and the drawer.
---
.../subsystems/frontend-primitives.md | 6 +-
.../internal/subsystems/unified-resources.md | 13 +-
.../0911e211ce898d75.json | 45 ++++
.../truenas/TrueNASStorageTopologyTable.tsx | 99 ++++-----
.../TrueNASStorageTopologyTable.sort.test.tsx | 67 ++++++
.../TrueNASStorageTopologyTable.test.tsx | 66 +++++-
.../__tests__/truenasPageModel.test.ts | 195 ++++++++++++++++++
.../src/features/truenas/truenasPageModel.ts | 83 ++++++++
8 files changed, 523 insertions(+), 51 deletions(-)
create mode 100644 frontend-modern/browser-verification/0911e211ce898d75.json
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index e1a0659ebf..b0f5f3b318 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -660,9 +660,11 @@ The TrueNAS Storage `Storage type` scope follows the same boundary with stable
the current option through pressed-state semantics, preserves the scope in the
URL across reload, and removes the query value for the default `all` state.
On narrow screens the physical-disk scope may reprioritize its canonical table
-columns to endurance, temperature, and health while retaining the shared table
+columns to endurance and temperature while retaining the shared table
overflow and touch-target behavior; it must not introduce a second mobile-only
-filter or table shell.
+filter or table shell. Its Health column stays desktop-only in every scope:
+the status dot carries a disk's condition on a phone, the row drawer carries
+the reason, and the old pill clipped past the phone container edge.
Large-estate platform pages must keep one canonical inventory snapshot for the
initial read and explicit refresh path. The Proxmox and VMware vSphere
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index e1d02270e6..13c7e2f4c8 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -960,7 +960,18 @@ before text and status filters so metadata from an excluded disk cannot retain
its pool or dataset ancestor. Physical-disk presentation may prioritize the
canonical SMART health, temperature, and endurance evidence; absent wearout or
percentage-used telemetry remains unknown rather than being rendered as zero
-remaining life.
+remaining life. Storage health is exception-first: `getTrueNASStorageIssue`
+returns nothing for healthy pools, datasets, and disks, so their Health cell
+stays empty and the status dot alone says healthy. A non-healthy row carries
+the reasons TrueNAS gave, in order: native TrueNAS alert text on that
+resource, storage and physical-disk risk summaries, then the dataset state tag
+(`state:locked`, `state:unmounted`, `state:readonly`, which is the only place
+the provider says why a dataset is impaired). Without any of those it falls
+back to the pool state, the disk health value, or an impaired TrueNAS source.
+The first reason renders in the desktop Health column on the row's single line,
+coloured by bucket, with a count for the rest and the full list in the title
+and screen-reader text. Phones keep the status dot and reach the reason through
+the row drawer. Health sorts by severity rank, not label text.
That shell also owns responsive width composition. Source tables may declare
desktop breakpoint floors, but the phone branch must fit its selected columns
inside the available container without a horizontal rail. Each unified-resource
diff --git a/frontend-modern/browser-verification/0911e211ce898d75.json b/frontend-modern/browser-verification/0911e211ce898d75.json
new file mode 100644
index 0000000000..cc8854b12c
--- /dev/null
+++ b/frontend-modern/browser-verification/0911e211ce898d75.json
@@ -0,0 +1,45 @@
+{
+ "version": 1,
+ "base_sha": "fdcffc64114bd9d7ff7a92324391a09702c3c26a",
+ "verified_at": "2026-10-05T05:37:16Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "19e1e5b705285464607804f8a8d7b5174e39c856290bd8c6ce5dc5e1212ff2af",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts": "b1955834430d00fca3fc4176614a83f4a8387c8c29a85f0f17c6fd1360faf514"
+ },
+ "routes": [
+ "/truenas/storage",
+ "/truenas/storage?kind=disks"
+ ],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900
+ },
+ {
+ "width": 820,
+ "height": 700
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Storage at 1440: every row is single-line. 18 healthy rows leave Health empty, where before each carried a green Healthy pill. The archive pool reads Pool archive is DEGRADED: one member of archive mirror-0 is reporting checksum and SMART faults (truncated, +1 for the copied SMART alert, full list on hover), archive/cold reads Dataset is read-only, sdc reads Device /dev/sdc has SMART test failures, coloured amber in the Health column",
+ "Health sorts by severity rank (Attention, Offline, Unknown, Healthy), not by label text",
+ "Disk scope at 1440: Endurance and Temp visible, sdc reason in Health",
+ "Tablet at 820 and dark mode at 1440: reasons truncate cleanly with the +1 kept",
+ "Phone at 390: Resource, Kind, Usage, Disks with status dots, Health hidden, so the old pill no longer clips past the edge in the disk scope",
+ "Phone drawer: tapping archive shows DEGRADED and tapping sdc shows the SMART failure. The read-only dataset drawer still only says Degraded, a pre-existing drawer gap tracked for a follow-up",
+ "Rebuilt as one commit on current main after the services and VMs change landed. The two frontend files are byte-identical to the version verified above, and the page was reshot on this tree"
+ ],
+ "interactions": [
+ "Switched to the Physical disks scope",
+ "Tapped archive, archive/cold and sdc on a 390 phone and read each drawer"
+ ]
+}
diff --git a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
index 2e5405b673..492c1c4c8e 100644
--- a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
+++ b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
@@ -18,7 +18,6 @@ import {
createPlatformTableFilterState,
createPlatformTableSortState,
formatPlatformTableBytesValue,
- formatPlatformTableTitleCaseValue,
getPlatformTableCellClassForKind,
type PlatformTableFilterOption,
type PlatformTableSortState,
@@ -39,7 +38,8 @@ import {
filterTrueNASStorageTopologyRows,
filterTrueNASStorageTopologyRowsByKind,
getTrueNASResourceDisplayStatus,
- mapTrueNASStorageStatus,
+ getTrueNASStorageIssue,
+ type TrueNASStorageIssue,
type TrueNASStorageStatusFilter,
type TrueNASStorageKindFilter,
type TrueNASStorageTopologyKind,
@@ -193,44 +193,52 @@ const DiskEnduranceCell: Component<{ row: TrueNASStorageTopologyRow }> = (props)
);
};
-const riskLabel = (row: TrueNASStorageTopologyRow): string => {
- const risk =
- asTrimmedString(row.resource.storage?.risk?.level) ||
- asTrimmedString(row.resource.physicalDisk?.risk?.level);
- if (risk) return formatPlatformTableTitleCaseValue(risk);
- const incidentCount = row.resource.incidentCount ?? row.resource.incidents?.length ?? 0;
- if (incidentCount > 0) return `${incidentCount} alert${incidentCount === 1 ? '' : 's'}`;
- const mapped = mapTrueNASStorageStatus(row.resource);
- if (mapped === 'attention') return 'Attention';
- if (mapped === 'offline') return 'Offline';
- if (mapped === 'healthy') return 'Healthy';
- return 'Unknown';
+const issueReasonClass = (issue: TrueNASStorageIssue): string => {
+ if (issue.status === 'attention') return 'text-amber-700 dark:text-amber-300';
+ if (issue.status === 'offline') return 'text-red-600 dark:text-red-300';
+ return 'text-muted';
};
-const riskPillClass = (row: TrueNASStorageTopologyRow): string => {
- const mapped = mapTrueNASStorageStatus(row.resource);
- if (mapped === 'attention') {
- return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
- }
- if (mapped === 'offline') {
- return 'border-red-300/50 bg-red-500/10 text-red-700 dark:text-red-300';
- }
- if (mapped === 'healthy') {
- return 'border-emerald-300/50 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
- }
- return 'border-border bg-surface-alt text-muted';
-};
+// The visible reason is truncated and abbreviates the rest to a count, so
+// assistive technology gets the bucket and every reason instead.
+const issueScreenReaderText = (issue: TrueNASStorageIssue): string =>
+ `${issue.label}: ${issue.reasons.map((reason) => reason.replace(/[.\s]+$/, '')).join('. ')}.`;
-const RiskPill: Component<{ row: TrueNASStorageTopologyRow }> = (props) => (
-
- {riskLabel(props.row)}
-
+// Healthy rows leave the Health column empty: the status dot already says
+// healthy, and a column of green pills buried the few rows that are not. An
+// exception shows why, on the row's single line (the shared platform-table
+// rhythm), with every reason on hover and in the row drawer. The colour
+// carries the bucket, so a pill repeating Attention would only take room
+// from the reason.
+const IssueSummary: Component<{ issue: TrueNASStorageIssue | null }> = (props) => (
+
+ {(issue) => (
+