From 3d77bb5b3c4ed29ee7475d3eb771f80f2fbcd7fc Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 11:17:00 +0100
Subject: [PATCH] Keep physical disk rows current across live snapshots
The keyed platform renderer preserves row owners, but the disk table captured mount-time presentation. Derive current health, readings, placement and target bindings reactively without discarding focus or expanded detail. Cover snapshot replacement, in-place updates, missing evidence and attention-filter recovery; record desktop and phone browser acceptance.
Change-source: pulse-maintainer
---
.../internal/subsystems/storage-recovery.md | 16 ++
.../browser-tests/disk-live-refresh.cjs | 177 ++++++++++++++++++
.../browser-tests/disk-live-refresh.html | 12 ++
.../browser-tests/disk-live-refresh.tsx | 116 ++++++++++++
frontend-modern/browser-verification.json | 44 +++--
.../src/components/Storage/DiskList.tsx | 88 +++++----
.../Storage/__tests__/DiskList.test.tsx | 156 ++++++++++++++-
7 files changed, 548 insertions(+), 61 deletions(-)
create mode 100644 frontend-modern/browser-tests/disk-live-refresh.cjs
create mode 100644 frontend-modern/browser-tests/disk-live-refresh.html
create mode 100644 frontend-modern/browser-tests/disk-live-refresh.tsx
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 987a1e183..6431645e1 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -6245,3 +6245,19 @@ state. The Recovery settings item and its capability gate are untouched.
`internal/api/onboarding_handlers.go` changed only the human-readable pairing
readiness messages so they name Settings > Pulse Mobile. This opens no new
storage or recovery path.
+
+### Live physical-disk row continuity
+
+Physical Disks uses the canonical keyed windowed renderer, which retains row
+owners across resource snapshots. Disk presentation must therefore stay
+reactive: health and its evidence, temperature, endurance, capacity, device,
+host and placement labels reflect the latest snapshot rather than the mount-time
+sample. Removed readings revert to their existing unknown/placeholders without
+retaining old warning text or colour. Metric summary identities, highlight
+matching and disclosure control IDs track the current canonical disk target.
+Unchanged resource IDs keep their mounted row, focus and expanded detail;
+attention filtering and the displayed health must agree through fault/recovery.
+This changes no collector, health thresholds, history policy or server identity.
+`frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx` exercises
+snapshot replacement, in-place reconciliation, missing evidence, target changes
+and fault/recovery using the real keyed renderer.
diff --git a/frontend-modern/browser-tests/disk-live-refresh.cjs b/frontend-modern/browser-tests/disk-live-refresh.cjs
new file mode 100644
index 000000000..11f7d2237
--- /dev/null
+++ b/frontend-modern/browser-tests/disk-live-refresh.cjs
@@ -0,0 +1,177 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { createHash } = require('node:crypto');
+const { chromium, webkit } = require('playwright');
+
+(async () => {
+ const root = '/workspace/frontend-modern';
+ process.chdir(root);
+ const artifacts = path.join(root, 'node_modules/disk-live-refresh-proof');
+ fs.mkdirSync(artifacts, { recursive: true });
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ root,
+ configFile: path.join(root, 'vite.config.ts'),
+ cacheDir: fs.mkdtempSync(path.join(artifacts, 'vite-cache-')),
+ server: { host: '127.0.0.1', port: 5221, strictPort: true },
+ });
+ const observations = [];
+ let browser;
+ try {
+ await server.listen();
+ for (const scenario of [
+ { engine: chromium, name: 'chromium', width: 1365, height: 900, dark: false },
+ { engine: webkit, name: 'webkit', width: 390, height: 844, dark: true },
+ ]) {
+ browser = await scenario.engine.launch(
+ scenario.name === 'chromium'
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({
+ viewport: { width: scenario.width, height: scenario.height },
+ isMobile: scenario.name === 'webkit',
+ hasTouch: scenario.name === 'webkit',
+ });
+ const errors = [],
+ requests = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ await page.route('**/*', async (route) => {
+ const url = new URL(route.request().url());
+ if (url.origin !== 'http://127.0.0.1:5221') return route.abort();
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ requests.push(url.pathname);
+ if (url.pathname === '/api/license/runtime-capabilities')
+ return route.fulfill({
+ json: {
+ capabilities: [],
+ limits: [],
+ max_history_days: 7,
+ hosted_mode: false,
+ runtime: { build: 'community', label: 'Pulse Community runtime' },
+ blocked_capabilities: [],
+ },
+ });
+ return route.fulfill({ json: { data: [], enabled: false } });
+ });
+ await page.goto('http://127.0.0.1:5221/browser-tests/disk-live-refresh.html');
+ await page.evaluate(
+ (dark) => document.documentElement.classList.toggle('dark', dark),
+ scenario.dark,
+ );
+ const fixture = page.getByTestId('disk-live-refresh-fixture');
+ const row = fixture.locator('[data-row-id="disk-one"]');
+ await row.getByText('Healthy', { exact: true }).waitFor();
+ await row.getByText('Archive SSD', { exact: true }).click();
+ const disclosure = row.getByRole('button', { name: 'Collapse Archive SSD', exact: true });
+ await disclosure.focus();
+ await row.evaluate((element) => {
+ element.dataset.proofOwner = 'retained';
+ });
+ const initialControls = await disclosure.getAttribute('aria-controls');
+ const snapshot = (phase) =>
+ page.locator(`button[data-update="${phase}"]`).evaluate((button) => button.click());
+ const checkContinuity = async () => {
+ assert.equal(await row.getAttribute('data-proof-owner'), 'retained');
+ assert.equal(
+ await row.locator('button').evaluate((button) => document.activeElement === button),
+ true,
+ );
+ assert.equal(await row.locator('button').getAttribute('aria-expanded'), 'true');
+ const controls = await row.locator('button').getAttribute('aria-controls');
+ assert.equal(await page.locator(`[id="${controls}"]`).count(), 1);
+ assert.equal(await fixture.locator('[data-inline-detail-for]').count(), 1);
+ const dimensions = await page.evaluate(() => ({
+ scroll: document.documentElement.scrollWidth,
+ width: innerWidth,
+ }));
+ assert.ok(dimensions.scroll <= dimensions.width + 1, JSON.stringify(dimensions));
+ return dimensions;
+ };
+ const screenshot = (state) =>
+ fixture.screenshot({ path: path.join(artifacts, `${scenario.name}-${state}.png`) });
+
+ await snapshot('fault');
+ await row.getByText('Replace Now', { exact: true }).waitFor();
+ for (const text of ['Archive SSD (fault)', '4%', '63°C'])
+ assert.equal(await row.getByText(text, { exact: true }).count(), 1);
+ assert.equal(await row.getByText('Healthy', { exact: true }).count(), 0);
+ assert.equal(await row.getAttribute('data-summary-series-id'), 'agent-archive:sdz');
+ assert.notEqual(await row.locator('button').getAttribute('aria-controls'), initialControls);
+ await fixture.getByText('2', { exact: true }).waitFor();
+ const faultDimensions = await checkContinuity();
+ await screenshot('fault');
+
+ await snapshot('missing');
+ await row.getByText('Unknown', { exact: true }).waitFor();
+ for (const text of ['Replace Now', 'SMART failed.', '4%', '63°C'])
+ assert.equal(await row.getByText(text, { exact: true }).count(), 0);
+ for (const column of ['temp', 'life', 'size', 'role', 'parent'])
+ assert.equal(
+ (await row.locator(`td[data-storage-column="${column}"]`).innerText()).trim(),
+ '—',
+ );
+ await fixture
+ .getByText('Temperature is temporarily unavailable: No current reading', { exact: true })
+ .waitFor();
+ const missingDimensions = await checkContinuity();
+ await screenshot('missing');
+
+ await snapshot('healthy');
+ await row.getByText('Healthy', { exact: true }).waitFor();
+ assert.equal(await row.getByText('96%', { exact: true }).count(), 1);
+ assert.equal(await row.getByText('41°C', { exact: true }).count(), 1);
+ assert.equal(await row.getAttribute('data-summary-series-id'), 'agent-archive:sda');
+ assert.equal(await row.locator('button').getAttribute('aria-controls'), initialControls);
+ const recoveredDimensions = await checkContinuity();
+ await screenshot('recovered');
+
+ await snapshot('fault');
+ await row.getByText('Replace Now', { exact: true }).waitFor();
+ await page.locator('[data-filter="attention"]').evaluate((button) => button.click());
+ assert.equal(await row.getByText('Replace Now', { exact: true }).count(), 1);
+ await snapshot('healthy');
+ await fixture.getByText('No disks need attention', { exact: true }).waitFor();
+ await page.locator('[data-filter="all"]').evaluate((button) => button.click());
+ await row.getByText('Healthy', { exact: true }).waitFor();
+ assert.deepEqual(errors, []);
+ observations.push({
+ browser: scenario.name,
+ version: browser.version(),
+ viewport: { width: scenario.width, height: scenario.height },
+ dark: scenario.dark,
+ faultDimensions,
+ missingDimensions,
+ recoveredDimensions,
+ errors,
+ requests,
+ });
+ await browser.close();
+ browser = undefined;
+ }
+ const source = 'src/components/Storage/DiskList.tsx';
+ const result = {
+ result: 'passed',
+ playwright: require('playwright/package.json').version,
+ content_sha256: {
+ [`frontend-modern/${source}`]: createHash('sha256')
+ .update(fs.readFileSync(path.join(root, source)))
+ .digest('hex'),
+ },
+ observations,
+ };
+ fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n');
+ console.log(JSON.stringify(result));
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ for (const name of fs.readdirSync(artifacts)) {
+ const file = path.join(artifacts, name);
+ if (fs.statSync(file).isFile()) fs.chmodSync(file, 0o644);
+ }
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/disk-live-refresh.html b/frontend-modern/browser-tests/disk-live-refresh.html
new file mode 100644
index 000000000..e3fc664d8
--- /dev/null
+++ b/frontend-modern/browser-tests/disk-live-refresh.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Physical disk live refresh verification
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/disk-live-refresh.tsx b/frontend-modern/browser-tests/disk-live-refresh.tsx
new file mode 100644
index 000000000..e7a7dc87d
--- /dev/null
+++ b/frontend-modern/browser-tests/disk-live-refresh.tsx
@@ -0,0 +1,116 @@
+// Real DiskList/DiskDetail and keyed renderer, with synthetic collector snapshots.
+// This is browser behaviour evidence, not an installed SMART or appliance result.
+import { createSignal } from 'solid-js';
+import { render } from 'solid-js/web';
+import { DiskList } from '../src/components/Storage/DiskList';
+import type { StorageHealthFilter } from '../src/features/storageBackups/models';
+import type { Resource } from '../src/types/resource';
+import '../src/index.css';
+
+const disk = (phase: 'healthy' | 'fault' | 'missing'): Resource => ({
+ id: 'disk-one',
+ type: 'physical_disk',
+ name: 'Archive disk',
+ platformType: 'proxmox-pbs',
+ sourceType: 'agent',
+ status: 'online',
+ lastSeen: Date.now(),
+ metricsTarget: {
+ resourceType: 'disk',
+ resourceId: phase === 'healthy' ? 'agent-archive:sda' : 'agent-archive:sdz',
+ },
+ identity: { hostname: 'archive-host' },
+ canonicalIdentity: { hostname: 'archive-host' },
+ physicalDisk: {
+ model: phase === 'fault' ? 'Archive SSD (fault)' : 'Archive SSD',
+ devPath: phase === 'healthy' ? '/dev/sda' : '/dev/sdz',
+ serial: 'SYNTHETIC-ONE',
+ diskType: phase === 'missing' ? undefined : 'ssd',
+ health: phase === 'healthy' ? 'PASSED' : phase === 'fault' ? 'FAILED' : 'UNKNOWN',
+ wearout: phase === 'healthy' ? 96 : phase === 'fault' ? 4 : -1,
+ temperature: phase === 'healthy' ? 41 : phase === 'fault' ? 63 : 0,
+ sizeBytes: phase === 'missing' ? 0 : 2_000_000_000_000,
+ storageRole: phase === 'missing' ? undefined : 'cache_pool',
+ storageGroup: phase === 'missing' ? undefined : 'Archive Pool',
+ risk:
+ phase === 'fault'
+ ? {
+ level: 'critical',
+ reasons: [{ code: 'smart-failed', severity: 'critical', summary: 'SMART failed.' }],
+ }
+ : undefined,
+ smart:
+ phase === 'missing'
+ ? undefined
+ : { powerOnHours: 100, pendingSectors: phase === 'fault' ? 2 : 0 },
+ collection:
+ phase === 'missing'
+ ? {
+ temperature: { state: 'unavailable', source: 'fixture', reason: 'No current reading' },
+ }
+ : undefined,
+ },
+});
+
+const Fixture = () => {
+ const [disks, setDisks] = createSignal([disk('healthy')]);
+ const [selectedDiskId, setSelectedDiskId] = createSignal(null);
+ const [healthFilter, setHealthFilter] = createSignal('all');
+ return (
+
+ Physical disk live refresh verification
+
+ Synthetic snapshots in the production PBS disk table and detail.
+
+
+
+
+
+
+
+
+
+
+ );
+};
+render(() => , document.getElementById('root')!);
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index 45fb502a2..bc8f3ba8b 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,49 +1,53 @@
{
"version": 1,
- "base_sha": "1a0cd7c9d5aa02de754535a293dc0cacd4ef6fa0",
- "verified_at": "2026-10-01T18:27:14.912695Z",
+ "base_sha": "33d6d729c1dc7c0913a970fd4c463a9f9575b7b1",
+ "verified_at": "2026-10-02T10:16:57.632722Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/features/docs/docMarkdown.ts"
+ "frontend-modern/src/components/Storage/DiskList.tsx"
],
"content_sha256": {
- "frontend-modern/src/features/docs/docMarkdown.ts": "572a3b4bc5bc1114adef5aa64bb0ea833c0dbe5865bbd57d3dc767fd1965aa55"
+ "frontend-modern/src/components/Storage/DiskList.tsx": "df63db162779ff0d2064475a9db1fd67ac748703f398a88d108ecdd154dae9d2"
},
"routes": [
- "/docs/PULSE_PRO",
- "/browser-tests/docs-fragment-navigation.html?scenario=plans"
+ "/browser-tests/disk-live-refresh.html"
],
"viewports": [
{
- "width": 1280,
+ "width": 1365,
"height": 900
},
{
"width": 390,
- "height": 900
+ "height": 844
}
],
"states": [
- "Production Docs page/router/styles with the current shipped eight-column plan comparison, not a replacement table fixture",
- "Every sanitized thead TH has trusted scope=col and all eight native columnheader roles; document role/scope/class remain stripped",
- "Desktop Chromium light and phone-emulated WebKit dark; table-local horizontal scrolling and outer-page containment; four final full-page/header screenshots inspected",
- "This establishes browser accessibility roles, not screen-reader speech or published availability"
+ "Production DiskList, DiskDetail, keyed renderer and styles with synthetic PBS collector snapshots, not installed appliance evidence",
+ "Healthy to failed disk updates health, warning text, endurance, temperature, model and canonical target without remounting the row or its expanded detail",
+ "Missing measurements remove obsolete values and critical styling; unavailable detail evidence remains visible; recovery restores current values",
+ "Disclosure keyboard focus and aria-controls survive same-ID updates; changed metric targets update summary/detail linkage",
+ "Attention filter includes the failed disk, removes the recovered disk and returns current healthy values in All",
+ "Desktop Chromium light and phone-emulated WebKit dark have no outer-page overflow or page errors; six fault/missing/recovered screenshots inspected; existing dense phone truncation is unchanged"
],
"interactions": [
- "Navigate to the real plans document and inspect table DOM and accessibility snapshot",
- "On phone, scroll the table horizontally without moving the outer page; bring its header into view"
+ "Expand the real disk row and focus its disclosure, apply fault/missing/recovery snapshots without navigating or reloading",
+ "Check current values, stable owner, focus, live disclosure target and selected detail after each snapshot",
+ "Toggle Attention/All across fault and recovery"
],
- "command": "pulse-worker-browser frontend-modern/browser-tests/docs-table-headers.cjs",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/disk-live-refresh.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/chromium-after.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/chromium-headers.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/webkit-after.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/webkit-headers.png",
- "/var/lib/pulse-maintainer/worker-outputs/web-product-yqww6hbj/browser/docs/after.json"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/chromium-fault.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/chromium-missing.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/chromium-recovered.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/webkit-fault.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/webkit-missing.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/webkit-recovered.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-zuzev1gs/browser/result.json"
]
}
diff --git a/frontend-modern/src/components/Storage/DiskList.tsx b/frontend-modern/src/components/Storage/DiskList.tsx
index 280ebce9c..571539031 100644
--- a/frontend-modern/src/components/Storage/DiskList.tsx
+++ b/frontend-modern/src/components/Storage/DiskList.tsx
@@ -320,15 +320,20 @@ export const DiskList: Component = (props) => {
{(disk) => {
- const data = model.getDiskData(disk);
- const status = getPhysicalDiskHealthStatus(data);
- const hostLabel = getPhysicalDiskHostLabel(data, disk);
- const healthSummary = getPhysicalDiskHealthSummary(status);
+ // Windowed rows retain their owner by resource ID across live
+ // snapshots. Derive the presentation reactively, not just when
+ // the row first mounts, so health and identity cannot go stale.
+ const data = createMemo(() => model.getDiskData(disk));
+ const status = createMemo(() => getPhysicalDiskHealthStatus(data()));
+ const hostLabel = createMemo(() => getPhysicalDiskHostLabel(data(), disk));
+ const healthSummary = createMemo(() => getPhysicalDiskHealthSummary(status()));
const isSelected = () => model.selectedDisk()?.id === disk.id;
- const summarySeriesId = resolvePhysicalDiskMetricResourceId(disk);
+ const summarySeriesId = createMemo(() => resolvePhysicalDiskMetricResourceId(disk));
const isSummaryHighlighted = () =>
- props.highlightedSummarySeriesId === summarySeriesId;
- const detailControlsId = buildSummaryDisclosureControlsId(summarySeriesId);
+ props.highlightedSummarySeriesId === summarySeriesId();
+ const detailControlsId = createMemo(() =>
+ buildSummaryDisclosureControlsId(summarySeriesId()),
+ );
const interactiveRowHandlers = createSummaryInteractiveRowPreviewHandlers({
onPreview: () => props.onHoverChange?.(disk.id),
onPreviewClear: () => props.onHoverChange?.(null),
@@ -338,7 +343,7 @@ export const DiskList: Component = (props) => {
<>
= (props) => {
model.toggleSelectedDisk(disk)}
onPreviewClear={() => props.onHoverChange?.(null)}
/>
- {data.model || 'Unknown Disk'}
+ {data().model || 'Unknown Disk'}
@@ -379,11 +384,11 @@ export const DiskList: Component = (props) => {
data-storage-column="device"
>
—}
>
-
- {data.devPath}
+
+ {data().devPath}
@@ -393,11 +398,11 @@ export const DiskList: Component = (props) => {
data-storage-column="host"
>
—}
>
-
- {hostLabel}
+
+ {hostLabel()}
@@ -407,14 +412,14 @@ export const DiskList: Component = (props) => {
data-storage-column="role"
>
—}
>
- {getPhysicalDiskRoleLabel(data)}
+ {getPhysicalDiskRoleLabel(data())}
@@ -424,14 +429,14 @@ export const DiskList: Component = (props) => {
data-storage-column="parent"
>
—}
>
- {getPhysicalDiskParentLabel(data)}
+ {getPhysicalDiskParentLabel(data())}
@@ -441,12 +446,15 @@ export const DiskList: Component = (props) => {
data-storage-column="health"
>
-
- {status.label}
+
+ {status().label}
-
-
- {healthSummary}
+
+
+ {healthSummary()}
@@ -457,13 +465,13 @@ export const DiskList: Component = (props) => {
data-storage-column="life"
>
—}
>
- {getPhysicalDiskLifeLabel(data)}
+ {getPhysicalDiskLifeLabel(data())}
@@ -473,17 +481,17 @@ export const DiskList: Component = (props) => {
data-storage-column="temp"
>
0}
+ when={data().temperature > 0}
fallback={—}
>
- {formatTemperature(data.temperature)}
+ {formatTemperature(data().temperature)}
@@ -493,7 +501,7 @@ export const DiskList: Component = (props) => {
data-storage-column="size"
>
0}
+ when={data().size > 0}
fallback={
= (props) => {
}
>
- {formatBytes(data.size)}
+ {formatBytes(data().size)}
-
+
diff --git a/frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx b/frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx
index 0956cb917..c8b0ad943 100644
--- a/frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx
+++ b/frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx
@@ -1,5 +1,6 @@
-import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
+import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
+import { createStore, reconcile } from 'solid-js/store';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Resource } from '@/types/resource';
import { DiskList } from '@/components/Storage/DiskList';
@@ -83,6 +84,159 @@ describe('DiskList', () => {
cleanup();
});
+ it('refreshes keyed disk rows without losing the expanded detail or keyboard focus', async () => {
+ const initial = buildDisk('sda', 'tower', { diskType: 'ssd', wearout: 96 });
+ const [disks, setDisks] = createSignal([initial]);
+ const [selectedDiskId, setSelectedDiskId] = createSignal('sda');
+ const view = render(() => (
+
+ ));
+ const row = view.container.querySelector('[data-row-id="sda"]')!;
+ const detail = screen.getByTestId('disk-detail');
+ const disclosure = within(row as HTMLElement).getByRole('button');
+ disclosure.focus();
+ const initialControls = disclosure.getAttribute('aria-controls');
+
+ setDisks([
+ {
+ ...buildDisk('sda', 'archive', {
+ model: 'Replacement SSD',
+ devPath: '/dev/sdz',
+ diskType: 'ssd',
+ wearout: 4,
+ sizeBytes: 4_000_000_000_000,
+ temperature: 63,
+ storageRole: 'cache_pool',
+ storageGroup: 'Archive Pool',
+ health: 'FAILED',
+ risk: {
+ level: 'critical',
+ reasons: [{ code: 'smart-failed', severity: 'critical', summary: 'SMART failed.' }],
+ },
+ }),
+ metricsTarget: { resourceType: 'disk', resourceId: 'disk:archive:sdz' },
+ },
+ ]);
+
+ await waitFor(() =>
+ expect(within(row as HTMLElement).getByText('Replace Now')).toBeInTheDocument(),
+ );
+ for (const text of [
+ 'Replacement SSD',
+ '/dev/sdz',
+ 'archive',
+ 'Cache Pool',
+ 'Archive Pool',
+ 'SMART failed.',
+ '4%',
+ '63°C',
+ ]) {
+ expect(within(row as HTMLElement).getByText(text)).toBeInTheDocument();
+ }
+ expect(within(row as HTMLElement).getByText('3.64 TB')).toBeInTheDocument();
+ expect(screen.queryByText('Healthy')).not.toBeInTheDocument();
+ expect(screen.queryByText('96%')).not.toBeInTheDocument();
+ expect(view.container.querySelector('[data-row-id="sda"]')).toBe(row);
+ expect(screen.getByTestId('disk-detail')).toBe(detail);
+ expect(document.activeElement).toBe(disclosure);
+ expect(disclosure.getAttribute('aria-label')).toContain('Replacement SSD');
+ expect(row).toHaveAttribute('data-summary-series-id', 'disk:archive:sdz');
+ expect(disclosure.getAttribute('aria-controls')).not.toBe(initialControls);
+ expect(
+ view.container.querySelector('[data-inline-detail-for="disk:archive:sdz"]'),
+ ).not.toBeNull();
+ expect(document.getElementById(disclosure.getAttribute('aria-controls')!)).not.toBeNull();
+ });
+
+ it('removes obsolete readings and fault styling when a keyed snapshot stops reporting them', async () => {
+ const [disks, setDisks] = createSignal([
+ buildDisk('sda', 'tower', {
+ health: 'FAILED',
+ diskType: 'ssd',
+ wearout: 4,
+ temperature: 63,
+ risk: {
+ level: 'critical',
+ reasons: [{ code: 'smart-failed', severity: 'critical', summary: 'SMART failed.' }],
+ },
+ }),
+ ]);
+ const view = render(() => (
+ {}}
+ />
+ ));
+ const row = view.container.querySelector('[data-row-id="sda"]')!;
+ expect(within(row as HTMLElement).getByText('Replace Now')).toHaveClass('text-red-700');
+ setDisks([
+ buildDisk('sda', 'tower', {
+ health: 'UNKNOWN',
+ diskType: '',
+ wearout: -1,
+ temperature: 0,
+ sizeBytes: 0,
+ storageRole: '',
+ storageGroup: '',
+ model: '',
+ devPath: '',
+ }),
+ ]);
+ await waitFor(() =>
+ expect(within(row as HTMLElement).getByText('Unknown')).toBeInTheDocument(),
+ );
+ for (const text of ['Replace Now', 'SMART failed.', '4%', '63°C', 'Parity', 'Tower Array']) {
+ expect(within(row as HTMLElement).queryByText(text)).not.toBeInTheDocument();
+ }
+ expect(within(row as HTMLElement).getByText('Unknown')).not.toHaveClass('text-red-700');
+ expect(within(row as HTMLElement).getByText('sda')).toBeInTheDocument();
+ for (const column of ['temp', 'life', 'size', 'device', 'role', 'parent']) {
+ expect(row.querySelector(`td[data-storage-column="${column}"]`)).toHaveTextContent('—');
+ }
+ expect(view.container.querySelector('[data-row-id="sda"]')).toBe(row);
+ });
+
+ it('keeps the attention filter and row health consistent during live updates and recovery', async () => {
+ const [disks, setDisks] = createStore({ items: [buildDisk('sda', 'tower')] });
+ const [healthFilter, setHealthFilter] = createSignal<'all' | 'attention'>('all');
+ const view = render(() => (
+ {}}
+ />
+ ));
+ const row = view.container.querySelector('[data-row-id="sda"]');
+ setDisks(
+ 'items',
+ reconcile([buildDisk('sda', 'tower', { smart: { pendingSectors: 2 }, temperature: 52 })]),
+ );
+ await waitFor(() => expect(screen.getByText('Needs Attention')).toBeInTheDocument());
+ expect(view.container.querySelector('[data-row-id="sda"]')).toBe(row);
+ setHealthFilter('attention');
+ expect(screen.getByText('Needs Attention')).toBeInTheDocument();
+ setDisks('items', reconcile([buildDisk('sda', 'tower')]));
+ await waitFor(() => expect(screen.getByText('No disks need attention')).toBeInTheDocument());
+ setHealthFilter('all');
+ await waitFor(() => expect(screen.getByText('Healthy')).toBeInTheDocument());
+ expect(screen.queryByText('52°C')).not.toBeInTheDocument();
+ });
+
it('renders physical disks in a single-line operational grid', () => {
renderDiskList({
disks: [