diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 73ded89bb5..536b4521b3 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,15 +1,13 @@ { "version": 1, - "base_sha": "6c7cd72cb2fd2dc4b769fb1b7da6423b4f37edb0", - "verified_at": "2026-10-02T22:36:43Z", + "base_sha": "572ea0158b7911bdef9e3405608c7a631b3d7648", + "verified_at": "2026-10-02T22:40:54Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Storage/StoragePoolRow.tsx", - "frontend-modern/src/features/storageBackups/storagePagePresentation.ts" + "frontend-modern/src/features/storageBackups/rowPresentation.ts" ], "content_sha256": { - "frontend-modern/src/components/Storage/StoragePoolRow.tsx": "09417822bd543ddbc312d0c15a4ea74971066d6623d421d889858e56262b9d3d", - "frontend-modern/src/features/storageBackups/storagePagePresentation.ts": "2a93a0176b912283c77b384ee2c66c4bc62c21a04b645eb93917a41740ad2d6e" + "frontend-modern/src/features/storageBackups/rowPresentation.ts": "cabd37e3f736467aecff68359fcb97be08bcabbe5c5ec6e0a0da2fe67d2e3261" }, "routes": [ "/proxmox/storage" @@ -19,25 +17,17 @@ "width": 1440, "height": 900 }, - { - "width": 600, - "height": 844 - }, { "width": 390, "height": 844 - }, - { - "width": 360, - "height": 844 } ], "states": [ - "Proxmox Storage tab pools table in compact (390), narrow (360), operational (600) and full (1440) container layouts with mock data including Online and Degraded pools", - "Pool rows at 390 measured per column with Range rects after the gutter change: name 126px for a 122px need, state 59 for 54, host 106 for 98, usage 39 for 28" + "Proxmox Storage tab pools table at 1440 (full layout, 13 rows) rendering the protection badges \"Protected\" and \"Backup Repository\" with matching titles and state words Online/Unknown", + "The mock backend did not emit its random ZFS-issue scenario (DEGRADED pool with \"resilver in progress\") in six boots during this pass, so the derived Resilvering/Scrubbing word was not observed live; it is pinned by rowPresentation.test.ts (scan string, scanDetails with percentage, and the non-ZFS fallback)" ], "interactions": [ - "Ran the Playwright scrollWidth-over-clientWidth sweep over every th/td leaf at 1440 (nothing reported), 600 (unchanged pre-existing operational host/protection clips), 390 (only the two long pool identities) and 360 (identity strings only)", - "Confirmed the !px-1 gutter applies only in the narrow and compact layouts and the operational/full column geometry is byte-identical to the previous sweep" + "Ran the Playwright scrollWidth-over-clientWidth sweep over every th/td leaf at 1440: nothing reported in the pools table", + "Listed every rendered protection badge text and title at 1440 to confirm non-rebuild labels and titles are unchanged by the compact-label change" ] } diff --git a/frontend-modern/src/features/storageBackups/__tests__/rowPresentation.test.ts b/frontend-modern/src/features/storageBackups/__tests__/rowPresentation.test.ts index 1afe8dff92..8787f8637d 100644 --- a/frontend-modern/src/features/storageBackups/__tests__/rowPresentation.test.ts +++ b/frontend-modern/src/features/storageBackups/__tests__/rowPresentation.test.ts @@ -101,6 +101,47 @@ describe('storage row presentation', () => { expect(getCompactStoragePoolProtectionTitle(record)).not.toContain('without parity'); }); + it('shows a zfs rebuild as its activity word and keeps the sentence as the title', () => { + const resilvering = { + ...baseRecord(), + rebuildInProgress: true, + protectionLabel: 'ZFS pool tank is resilvering', + details: { + zfsPool: { state: 'DEGRADED', scan: 'resilver in progress', devices: [] }, + }, + } as unknown as StorageRecord; + expect(getCompactStoragePoolProtectionLabel(resilvering)).toBe('Resilvering'); + expect(getCompactStoragePoolProtectionTitle(resilvering)).toBe('ZFS pool tank is resilvering'); + + const scrubbing = { + ...baseRecord(), + rebuildInProgress: true, + protectionLabel: 'ZFS pool tank scrub is running (45.2%)', + protectionSummary: 'ZFS pool tank scrub is running (45.2%)', + details: { + zfsPool: { + state: 'ONLINE', + scan: '', + scanDetails: { function: 'SCRUB', state: 'SCANNING', percentage: 45.2 }, + devices: [], + }, + }, + } as unknown as StorageRecord; + expect(getCompactStoragePoolProtectionLabel(scrubbing)).toBe('Scrubbing 45%'); + expect(getCompactStoragePoolProtectionTitle(scrubbing)).toBe( + 'ZFS pool tank scrub is running (45.2%)', + ); + + // A rebuild without zfs scan data keeps the label its platform supplied. + const parity = { + ...baseRecord(), + rebuildInProgress: true, + protectionLabel: 'Parity sync 12%', + } as unknown as StorageRecord; + expect(getCompactStoragePoolProtectionLabel(parity)).toBe('Parity sync 12%'); + expect(getCompactStoragePoolProtectionTitle(parity)).toBe('Parity sync 12%'); + }); + it('derives zfs issue fallback from pool state and errors', () => { const record = { ...baseRecord(), diff --git a/frontend-modern/src/features/storageBackups/rowPresentation.ts b/frontend-modern/src/features/storageBackups/rowPresentation.ts index 164517aaa2..f9bacbab41 100644 --- a/frontend-modern/src/features/storageBackups/rowPresentation.ts +++ b/frontend-modern/src/features/storageBackups/rowPresentation.ts @@ -66,9 +66,32 @@ export function getStoragePoolStateTextClass(record: StorageRecord): string { return 'text-base-content'; } +// The rebuild summary the backend promotes into the protection label is a +// sentence ("ZFS pool tank is resilvering (45.2%)"). The protection column is +// a badge, so a ZFS scan reads as the activity word, derived from the pool's +// own scan state (the same source the backend keys its risk reason on); the +// sentence stays available as the cell title. Non-ZFS rebuilds keep the label +// their platform supplies. +export function getStoragePoolRebuildLabel(record: StorageRecord): string { + const pool = getStorageRecordZfsPool(record); + if (!pool) return ''; + const scan = `${pool.scanDetails?.function || ''} ${pool.scan || ''}`.toLowerCase(); + const percentage = pool.scanDetails?.percentage; + const progress = + typeof percentage === 'number' && Number.isFinite(percentage) && percentage > 0 + ? ` ${Math.round(percentage)}%` + : ''; + if (scan.includes('resilver')) return `Resilvering${progress}`; + if (scan.includes('scrub')) return `Scrubbing${progress}`; + return ''; +} + export function getCompactStoragePoolProtectionLabel(record: StorageRecord): string { const label = (record.protectionLabel || '').trim(); - if (record.rebuildInProgress || record.protectionReduced) { + if (record.rebuildInProgress) { + return getStoragePoolRebuildLabel(record) || label || '—'; + } + if (record.protectionReduced) { return label || '—'; } if (label && label.toLowerCase() !== 'healthy') { @@ -124,7 +147,9 @@ export function getCompactStoragePoolProtectionTitle(record: StorageRecord): str return issueSummary; } } - return label; + // When the badge shows a derived word, the platform's full label is the title. + const fullLabel = (record.protectionLabel || '').trim(); + return fullLabel && fullLabel.toLowerCase() !== label.toLowerCase() ? fullLabel : label; } export function getCompactStoragePoolImpactLabel(record: StorageRecord): string {