diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index ab0525f836..290a92b001 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -663,8 +663,8 @@ On narrow screens the physical-disk scope may reprioritize its canonical 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. Its Health column stays desktop-only in every scope:
-the status dot plus the reason line under the name already carry a disk's
-condition, and the pill clipped past the phone container edge.
+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
@@ -713,10 +713,7 @@ scrolling exposes it. Touch scrolling must remain compositor-native: windowed
renderers must not attach touch listeners or replace keyed rows before the
browser moves the page, and must update their runway only from the passive
native scroll event. Spacer geometry is structural only: no feature may present
-it as loading, pagination, or an intentionally blank data region. Table rows
-may differ in height (an exception row with a reason line under its name),
-because a table runway sizes its spacers from the average mounted row rather
-than its tallest leading row. Settings
+it as loading, pagination, or an intentionally blank data region. Settings
resource pickers, Availability target lists, Actions, alerts, and every
provider-native platform table share this contract. For table rows with unique
logical ids, or an explicit unique key extractor, `PlatformWindowedRows` owns a
diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
index bb4d490869..1a49ebcb1e 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -2555,15 +2555,11 @@ sparse live snapshots update fields without remounting an open inline drawer
or discarding its tab and form state. Rows without a unique logical ID retain
reference-keyed rendering rather than paying for guessed index identity.
Spacer geometry and the inverse scroll-to-index mapping must share one
-representative item height. A table runway, anchored on its top spacer row,
-measures every mounted row up to the bottom spacer and divides by the mounted
-item count, so a few taller rows (a reason line under an exception's name, an
-open detail row) count once instead of sizing every unmounted row and making
-the window jump as they scroll out. Other anchors sample several leading
-siblings and keep the tallest, because a short leading group header sampled
-alone collapses the estimate and lets the mounted window outrun the real scroll
-position, turning a small scroll into many rows of unmounting and making an
-estate appear to lose rows.
+representative item height. The controller samples several leading siblings and
+keeps the tallest, because a short leading group header sampled alone collapses
+the estimate and lets the mounted window outrun the real scroll position,
+turning a small scroll into many rows of unmounting and making an estate appear
+to lose rows.
The workload guest-row path now follows the same pattern: the render shell
stays in `frontend-modern/src/components/Workloads/GuestRow.tsx`, tooltip-backed
cell presentation lives in `frontend-modern/src/components/Workloads/GuestRowCells.tsx`,
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 4326ce42f2..95d593c3f3 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -968,9 +968,10 @@ 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 under the name on every width and the full list stays
-in the title, so a phone user learns why a row is amber without the Health
-column. Health sorts by severity rank, not label text.
+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/24363d2e58468e62.json b/frontend-modern/browser-verification/24363d2e58468e62.json
new file mode 100644
index 0000000000..78aa7affb2
--- /dev/null
+++ b/frontend-modern/browser-verification/24363d2e58468e62.json
@@ -0,0 +1,45 @@
+{
+ "version": 1,
+ "base_sha": "a99f3804a756acc293b3962fb9eaba2cbec04655",
+ "verified_at": "2026-10-05T05:22:24Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts": "e1cc0cb0f4087c5c66014d254421aeea6b02842795c41978e1b9847dcfcb5c77",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "19e1e5b705285464607804f8a8d7b5174e39c856290bd8c6ce5dc5e1212ff2af"
+ },
+ "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 again. Healthy rows leave Health empty, the archive pool reads Pool archive is DEGRADED: one member of archive mirror-0 is reporting checksum and SMART faults (truncated, +1, full list on hover), archive/cold reads Dataset is read-only, sdc reads Device /dev/sdc has SMART test failures, all in the Health column and coloured amber",
+ "Name cells hold only the name, so the reason no longer stacks under it",
+ "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: the table is the contract phone projection (Resource, Kind, Usage, Disks) with status dots, Health hidden",
+ "Phone drawer: tapping archive shows DEGRADED and tapping sdc shows the SMART failure. The read-only dataset drawer only says Degraded, a pre-existing drawer gap left for a follow-up because the drawer model is also changed by the open services and VMs pull request",
+ "usePlatformWindowedItems is back to its main content, with rows uniform again"
+ ],
+ "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/platformPage/__tests__/PlatformWindowedRows.test.tsx b/frontend-modern/src/features/platformPage/__tests__/PlatformWindowedRows.test.tsx
index 109ffa7e9e..6aa10beb60 100644
--- a/frontend-modern/src/features/platformPage/__tests__/PlatformWindowedRows.test.tsx
+++ b/frontend-modern/src/features/platformPage/__tests__/PlatformWindowedRows.test.tsx
@@ -263,63 +263,4 @@ describe('PlatformWindowedRows', () => {
}
}
});
-
- it('averages the mounted table rows so a few taller rows do not size every unmounted row', () => {
- // Exception rows can carry a reason line under the name and sort first.
- // Taking the tallest of the leading rows as every row's height made the
- // spacers overshoot and the window jump as rows scrolled out.
- const items = () => Array.from({ length: 41 }, (_, index) => index);
- const originalRect = Element.prototype.getBoundingClientRect;
- const originalVisibility = (Element.prototype as Element & { checkVisibility?: () => boolean })
- .checkVisibility;
- Element.prototype.getBoundingClientRect = function (this: Element) {
- const raw = (this as HTMLElement).getAttribute?.('data-height');
- const height = raw ? Number(raw) : 0;
- return {
- height,
- width: 0,
- top: 0,
- left: 0,
- right: 0,
- bottom: height,
- x: 0,
- y: 0,
- toJSON: () => ({}),
- } as DOMRect;
- };
- (Element.prototype as Element & { checkVisibility?: () => boolean }).checkVisibility = () =>
- true;
-
- try {
- const { container } = render(() => (
-
- ));
-
- const bottomSpacer = container.querySelector(
- '[data-platform-window-spacer="bottom"]',
- ) as HTMLElement;
- // 8 mounted rows measure 2 x 60 + 6 x 30 = 300px, 37.5px each, so the
- // 33 unmounted rows reserve 1237.5px rather than 33 x 60.
- expect(bottomSpacer.style.height).toBe('1237.5px');
- } finally {
- Element.prototype.getBoundingClientRect = originalRect;
- if (originalVisibility) {
- (Element.prototype as Element & { checkVisibility?: () => boolean }).checkVisibility =
- originalVisibility;
- } else {
- Reflect.deleteProperty(Element.prototype, 'checkVisibility');
- }
- }
- });
});
diff --git a/frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts b/frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts
index 636bca23a8..673794ef0a 100644
--- a/frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts
+++ b/frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts
@@ -23,37 +23,6 @@ const PHONE_WINDOW_SIZE = 36;
// collapsing the estimate and desynchronising the window from the scroll
// position, while uniform tables still measure their real row height.
const MEASUREMENT_SAMPLE_SIZE = 3;
-
-const measureTallestSampledSibling = (anchor: HTMLElement): number => {
- let measuredHeight = 0;
- let sibling = anchor.nextElementSibling;
- for (let sampled = 0; sampled < MEASUREMENT_SAMPLE_SIZE && sibling; sampled += 1) {
- const height = sibling.getBoundingClientRect().height;
- if (height > measuredHeight) measuredHeight = height;
- sibling = sibling.nextElementSibling;
- }
- return measuredHeight;
-};
-
-// A table runway sits between a top and a bottom spacer row, so the mounted
-// rows can be measured as a whole. Averaging them per item lets a few taller
-// rows (a reason line under the name, an open detail row) count once instead
-// of setting the height of every row off screen, which made the spacers
-// overshoot and the window jump as rows scrolled out. Returns 0 when the
-// anchor is not such a runway, so other surfaces keep the sampled estimate.
-const measureRenderedRunwayItemHeight = (anchor: HTMLElement, renderedItems: number): number => {
- if (anchor.tagName !== 'TR' || anchor.dataset.platformWindowSpacer !== 'top') return 0;
- if (renderedItems <= 0) return 0;
- let total = 0;
- let sibling = anchor.nextElementSibling;
- while (sibling && (sibling as HTMLElement).dataset.platformWindowSpacer !== 'bottom') {
- total += sibling.getBoundingClientRect().height;
- sibling = sibling.nextElementSibling;
- }
- if (!sibling || total <= 0) return 0;
- return total / renderedItems;
-};
-
export interface PlatformWindowedItemsOptions- {
items: Accessor;
estimatedItemHeight?: number;
@@ -104,9 +73,13 @@ export function usePlatformWindowedItems
- (options: PlatformWindowedItemsOpt
if (!anchor || isWindowedSurfaceHidden(anchor)) return;
if (measureItems) {
- const measuredHeight =
- measureRenderedRunwayItemHeight(anchor, visibleItems().length) ||
- measureTallestSampledSibling(anchor);
+ let measuredHeight = 0;
+ let sibling = anchor.nextElementSibling;
+ for (let sampled = 0; sampled < MEASUREMENT_SAMPLE_SIZE && sibling; sampled += 1) {
+ const height = sibling.getBoundingClientRect().height;
+ if (height > measuredHeight) measuredHeight = height;
+ sibling = sibling.nextElementSibling;
+ }
if (measuredHeight > 0) setEstimatedItemHeight(measuredHeight);
}
diff --git a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
index 84ba5737b2..492c1c4c8e 100644
--- a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
+++ b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
@@ -193,40 +193,41 @@ const DiskEnduranceCell: Component<{ row: TrueNASStorageTopologyRow }> = (props)
);
};
-const issuePillClass = (issue: TrueNASStorageIssue): string => {
- if (issue.status === 'attention') {
- return 'border-amber-300/50 bg-amber-500/10 text-amber-700 dark:text-amber-300';
- }
- if (issue.status === 'offline') {
- return 'border-red-300/50 bg-red-500/10 text-red-700 dark:text-red-300';
- }
- return 'border-border bg-surface-alt text-muted';
-};
-
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';
};
-// The visible reason is clamped and abbreviates the rest to a count, so
+// 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('. ')}.`;
// Healthy rows leave the Health column empty: the status dot already says
-// healthy, and a column of green pills buries the few rows that are not.
-const IssuePill: Component<{ issue: TrueNASStorageIssue | null }> = (props) => (
+// 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) => (
-
- {issue().label}
-
+
+ {issue().reasons[0] ?? issue().label}
+
+ 1}>
+
+ +{issue().reasons.length - 1}
+
+
+ {issueScreenReaderText(issue())}
+
)}
);
@@ -329,11 +330,9 @@ export const getTrueNASStorageTopologyIndentClass = (depth: number): string => {
return 'pl-8 sm:pl-16';
};
-const ResourceCell: Component<{
- row: TrueNASStorageTopologyRow;
- issue: TrueNASStorageIssue | null;
- detailToggle?: JSX.Element;
-}> = (props) => {
+const ResourceCell: Component<{ row: TrueNASStorageTopologyRow; detailToggle?: JSX.Element }> = (
+ props,
+) => {
const displayStatus = () => getTrueNASResourceDisplayStatus(props.row.resource);
const indicator = () => getSimpleStatusIndicator(displayStatus());
const name = () => resourceName(props.row.resource);
@@ -353,28 +352,6 @@ const ResourceCell: Component<{
>
{name()}
-
- {(issue) => (
-
-
- {issue().reasons[0]}
-
- 1}>
-
- +{issue().reasons.length - 1}
-
-
- {issueScreenReaderText(issue())}
-
- )}
-
);
@@ -512,7 +489,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="metric-bar"
sort={sort}
sortKey="usage"
- class="platform-table-mobile-w-25 md:w-[28%]"
+ class="platform-table-mobile-w-25 md:w-[20%]"
>
@@ -520,7 +497,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="numeric-value"
sort={sort}
sortKey={kindFilter() === 'disks' ? 'endurance' : 'disks'}
- class="platform-table-mobile-w-15 md:w-[8%]"
+ class="platform-table-mobile-w-15 md:w-[7%]"
>
{kindFilter() === 'disks' ? 'Endurance' : 'Disks'}
@@ -528,15 +505,15 @@ export const TrueNASStorageTopologyTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="temp"
- class={`${kindFilter() === 'disks' ? 'table-cell' : 'hidden lg:table-cell'} md:w-[8%]`}
+ class={`${kindFilter() === 'disks' ? 'table-cell' : 'hidden lg:table-cell'} md:w-[7%]`}
>
Temp
@@ -563,7 +540,6 @@ export const TrueNASStorageTopologyTable: Component<{
-
+
{
expect(container.querySelector('[data-truenas-storage-kind="disk"]')).toBeNull();
});
- it('shows health only for exceptions, with the reason under the name', () => {
+ it('shows health only for exceptions, with the reason in the Health cell', () => {
const pool = makeStorageResource({
id: 'pool-archive',
name: 'archive',
@@ -195,23 +195,23 @@ describe('TrueNASStorageTopologyTable', () => {
const poolRow = container.querySelector('[data-truenas-storage-resource="pool-archive"]');
const healthyRow = container.querySelector('[data-truenas-storage-resource="dataset-backups"]');
- const reason = poolRow?.querySelector('[data-truenas-storage-reason]');
- const visible = [...(reason?.querySelectorAll('[aria-hidden="true"]') ?? [])].map(
+ const health = poolRow?.querySelector('[data-truenas-storage-health="attention"]');
+ const visible = [...(health?.querySelectorAll('[aria-hidden="true"]') ?? [])].map(
(node) => node.textContent,
);
+ // The reason sits in the Health cell on the row's single line. The name
+ // cell carries only the name, per the shared platform-table rhythm.
+ expect(health?.closest('td')).toHaveClass('platform-table-phone-hidden');
expect(visible).toEqual(['Pool archive is DEGRADED: one member of mirror-0 is faulted.', '+1']);
- expect(reason?.querySelector('.sr-only')).toHaveTextContent(
+ expect(health?.querySelector('.sr-only')).toHaveTextContent(
'Attention: Pool archive is DEGRADED: one member of mirror-0 is faulted. Device /dev/sdc has SMART test failures.',
);
- expect(reason).toHaveAttribute(
+ expect(health).toHaveAttribute(
'title',
'Pool archive is DEGRADED: one member of mirror-0 is faulted.\nDevice /dev/sdc has SMART test failures.',
);
- expect(poolRow?.querySelector('[data-truenas-storage-health="attention"]')).toHaveTextContent(
- 'Attention',
- );
- expect(healthyRow?.querySelector('[data-truenas-storage-reason]')).toBeNull();
+ expect(poolRow?.querySelector('td')).not.toHaveTextContent('DEGRADED');
expect(healthyRow?.querySelector('[data-truenas-storage-health]')).toBeNull();
expect(healthyRow).not.toHaveTextContent('Healthy');
});