From 8b30ef2fa6bb8aa2f5b1136488b8a36c5290eaa9 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 4 Aug 2026 00:31:43 +0100 Subject: [PATCH] Reduce the Workloads Avail and Backup columns to their signal Both columns spent permanent width rendering the healthy state, so a fleet with nothing wrong read as two columns of identical repeated values. Avail printed a freshness suffix on every probed row. Freshness is Pulse vouching for its own probe pipeline rather than a fact about the guest, and it reads fresh on almost every row, so it now renders only when the probe data is stale or of unknown age. Avail also becomes toggleable. It was the only permanently pinned column in the table, which made operators who run no availability probes pay 56px for mostly empty cells with no way to hide them. Backup drew a bordered, filled pill with a compact age on every guest. The shield colour already carries the verdict and the configured threshold already decides what counts as acceptable, so the age was decoration on the rows nobody has to act on. A fresh backup is now a bare green shield, and the age, the pill chrome and the alerting colours are reserved for stale, critical and never. The exact timestamp stays one hover away in the tooltip and the age stays in the aria-label, so nothing is lost for assistive tech. Verified against the live backend at 1440x900 and 390x844 across 34 guests. Contract-Neutral: Workloads guest-row presentation only. GuestRowCells.tsx and guestRowModel.tsx are the contracted owners for cell presentation and column metadata in performance-and-scalability.md and this change extends them in place, keeps the shared workloadGuestPresentation helpers required by frontend-primitives.md, and moves no contract section, width rule, or payload field. --- frontend-modern/browser-verification.json | 36 ++++++----- .../components/Workloads/GuestRowCells.tsx | 30 ++++++--- .../__tests__/BackupStatusCell.test.tsx | 61 +++++++++++++++++++ .../Workloads/__tests__/GuestRow.test.tsx | 24 +++++++- .../components/Workloads/guestRowModel.tsx | 1 + 5 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 frontend-modern/src/components/Workloads/__tests__/BackupStatusCell.test.tsx diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index c51dd7ca1..7d2acc9e0 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,17 +1,19 @@ { "version": 1, - "base_sha": "de5a1b20df6e825dfc590dee334b8877569a74a2", - "verified_at": "2026-08-03T22:40:33Z", + "base_sha": "3a24d9694f3bb4062170149d02e0eb8241cf398b", + "verified_at": "2026-08-03T23:30:46Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/docker/DockerHostsTable.tsx", - "frontend-modern/src/features/docker/DockerPageSurface.tsx" + "frontend-modern/src/components/Workloads/GuestRowCells.tsx", + "frontend-modern/src/components/Workloads/guestRowModel.tsx" ], "content_sha256": { - "frontend-modern/src/features/docker/DockerHostsTable.tsx": "c4042eb72e027b97761f5cd02ba7021e0d7b3669e05e9db9f52af46ecb5bdbbf", - "frontend-modern/src/features/docker/DockerPageSurface.tsx": "f4fe3a202d92efaedf61409a4602340f82411ff01252cc399cff758eff23857f" + "frontend-modern/src/components/Workloads/GuestRowCells.tsx": "f34d05f6731bacafb07613ac982ba621865e0f3e1db7baae42cff7ac994917d8", + "frontend-modern/src/components/Workloads/guestRowModel.tsx": "6be3cc7e2958f3bd8ead420dac8467b11941b1037721e881a4f1e85e1f5c8cd1" }, - "routes": ["/standalone/machines", "/docker/overview"], + "routes": [ + "/proxmox" + ], "viewports": [ { "width": 1440, @@ -23,15 +25,19 @@ } ], "states": [ - "Desktop Docker Overview fully loaded with the scoped Docker hosts title and View all machines action", - "Desktop Machines inventory after following the View all machines action, showing all five current machines", - "Phone-width Docker Overview fully loaded with the scoped title and action aligned without clipping", - "Phone-width Machines inventory after following the View all machines action" + "Proxmox Overview grouped workload table against the live 7655 backend with 34 guests across delly, minipc and pi", + "Avail column populated for probed guests and blank for unprobed guests such as iventoy, ollama and tails-anon", + "Backup column with all 34 rows in the fresh state rendering a bare green shield with no drawn age and no pill chrome", + "View preferences popover open with the Columns section expanded and the new Avail toggle listed", + "Avail column hidden through the new toggle and then restored to the original hidden set" ], "interactions": [ - "Followed View all machines from Docker Overview at desktop width and confirmed navigation to /standalone/machines with all five rows", - "Repeated the View all machines handoff at phone width and confirmed the complete machine table rendered", - "Inspected the Docker hosts title, action placement, table containment, mobile navigation, and footer at both widths", - "Confirmed the browser console reported no warnings or errors during either responsive journey" + "Signed in at 1440x900 and read the Proxmox Overview table through DOM assertions on the availability cells and the backup status badges", + "Confirmed availability cells render bare latency such as 3ms, 1ms and 5ms with no trailing freshness suffix", + "Confirmed all 34 fresh backup badges render empty text, carry the green shield colour, and have no rounded-full pill chrome, while their aria-label still reports the backup age", + "Opened View then Columns, clicked the Avail checkbox off, and confirmed the availability header and every availability cell were removed and the preference persisted to workloadsHiddenColumns", + "Clicked Avail back on and confirmed workloadsHiddenColumns returned to its original value of aiContext, os, ip", + "Resized to 390x844 and confirmed both columns render unchanged with no horizontal document overflow", + "Read console messages filtered to errors and found none across both viewports" ] } diff --git a/frontend-modern/src/components/Workloads/GuestRowCells.tsx b/frontend-modern/src/components/Workloads/GuestRowCells.tsx index 288c9fa21..5cf3be70e 100644 --- a/frontend-modern/src/components/Workloads/GuestRowCells.tsx +++ b/frontend-modern/src/components/Workloads/GuestRowCells.tsx @@ -86,18 +86,24 @@ function getBackupAgeBadgeLabel( } function getBackupAgeBadgeClass(status: BackupInfo['status']): string { - const base = - 'inline-flex h-5 min-w-[3.25rem] items-center justify-center gap-1 rounded-full border px-1.5 text-[10px] font-semibold leading-none tabular-nums cursor-help'; + const layout = + 'inline-flex h-5 min-w-[3.25rem] items-center justify-center gap-1 px-1.5 text-[10px] font-semibold leading-none tabular-nums cursor-help'; + // A healthy backup only needs its shield. The colour already carries the + // verdict and the threshold already decides what counts as acceptable, so the + // exact age is decoration on the rows nobody has to act on; it stays in the + // tooltip and the aria-label. Age and pill chrome are reserved for the states + // that need attention. + const pill = `${layout} rounded-full border`; switch (status) { case 'fresh': - return `${base} border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-950/40 dark:text-green-300`; + return `${layout} text-green-600 dark:text-green-400`; case 'stale': - return `${base} border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/70 dark:bg-yellow-950/40 dark:text-yellow-300`; + return `${pill} border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-900/70 dark:bg-yellow-950/40 dark:text-yellow-300`; case 'critical': - return `${base} border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300`; + return `${pill} border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-950/40 dark:text-red-300`; case 'never': - return `${base} border-border bg-surface-alt text-muted`; + return `${pill} border-border bg-surface-alt text-muted`; } } @@ -326,7 +332,7 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine aria-label={ariaLabel()} > - {badgeLabel()} + + {badgeLabel()} + @@ -421,8 +429,12 @@ function AvailabilityProbeCell(props: { presentation: AvailabilityProbePresentat : result === 'not checked' ? 'pending' : result; + // 'fresh' is Pulse vouching for its own probe pipeline, not a fact about the + // guest. It is true on nearly every row, so printing it turns the column into + // repeated noise. Surface freshness only when it is a reason to distrust the + // result. const freshness = p().freshnessLabel === 'freshness unknown' ? 'unknown' : p().freshnessLabel; - return `${compactResult} · ${freshness}`; + return freshness === 'fresh' ? compactResult : `${compactResult} · ${freshness}`; }); return ( diff --git a/frontend-modern/src/components/Workloads/__tests__/BackupStatusCell.test.tsx b/frontend-modern/src/components/Workloads/__tests__/BackupStatusCell.test.tsx new file mode 100644 index 000000000..f22c33e12 --- /dev/null +++ b/frontend-modern/src/components/Workloads/__tests__/BackupStatusCell.test.tsx @@ -0,0 +1,61 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { render, cleanup } from '@solidjs/testing-library'; + +import { BackupStatusCell } from '../GuestRowCells'; + +const HOUR_MS = 60 * 60 * 1000; + +const renderCell = (lastBackup: string | number | null) => { + const { container } = render(() => ); + const badge = container.querySelector('span[aria-label^="Backup status"]'); + if (!badge) throw new Error('backup badge not rendered'); + return badge as HTMLElement; +}; + +describe('BackupStatusCell', () => { + afterEach(() => { + cleanup(); + }); + + // The healthy state is the overwhelming majority of rows. It carries the + // verdict in the shield colour alone; the age would be repeated decoration. + it('renders a bare green shield with no age text when the backup is fresh', () => { + const badge = renderCell(Date.now() - 2 * HOUR_MS); + + expect(badge.textContent?.trim()).toBe(''); + expect(badge.className).toContain('text-green-600'); + expect(badge.className).not.toContain('rounded-full'); + expect(badge.className).not.toContain('border'); + expect(badge.querySelector('svg')).not.toBeNull(); + }); + + it('keeps the age available to assistive tech even when it is not drawn', () => { + const badge = renderCell(Date.now() - 2 * HOUR_MS); + + expect(badge.getAttribute('aria-label')).toMatch(/^Backup status: fresh, last backup /); + }); + + it('draws the age and pill chrome when the backup is stale', () => { + const badge = renderCell(Date.now() - 48 * HOUR_MS); + + expect(badge.textContent?.trim()).not.toBe(''); + expect(badge.className).toContain('rounded-full'); + expect(badge.className).toContain('border-yellow-200'); + }); + + it('draws the age and pill chrome when the backup is critical', () => { + const badge = renderCell(Date.now() - 200 * HOUR_MS); + + expect(badge.textContent?.trim()).not.toBe(''); + expect(badge.className).toContain('rounded-full'); + expect(badge.className).toContain('border-red-200'); + }); + + it('labels a guest that has never been backed up', () => { + const badge = renderCell(null); + + expect(badge.textContent?.trim()).toBe('None'); + expect(badge.className).toContain('rounded-full'); + expect(badge.getAttribute('aria-label')).toBe('Backup status: never'); + }); +}); diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx index 4f96ae63f..3526ef3e4 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx @@ -1337,7 +1337,7 @@ describe('context column for PVE workloads', () => { }); describe('backup column', () => { - it('shows compact age text for supported guests with a backup', () => { + it('keeps a fresh backup age accessible without drawing it in the row', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-05-26T12:00:00Z')); @@ -1350,12 +1350,32 @@ describe('backup column', () => { visibleColumnIds: ['name', 'backup'], }); - expect(screen.getByText('5h')).toBeTruthy(); + // A healthy backup is carried by the shield colour alone. The age stays in + // the aria-label and the tooltip so nothing is lost. + expect(screen.queryByText('5h')).toBeNull(); expect( container.querySelector('[aria-label="Backup status: fresh, last backup 5 hours ago"]'), ).toBeTruthy(); }); + it('draws the compact age for supported guests whose backup is stale', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-05-26T12:00:00Z')); + + const { container } = renderGuestRow({ + guest: makeGuest({ + type: 'qemu', + workloadType: 'vm', + lastBackup: Date.parse('2026-05-24T12:00:00Z'), + }), + visibleColumnIds: ['name', 'backup'], + }); + + const badge = container.querySelector('[aria-label^="Backup status: stale"]'); + expect(badge).toBeTruthy(); + expect(badge?.textContent?.trim()).toMatch(/\d/); + }); + it('shows None for supported guests without a backup', () => { renderGuestRow({ guest: makeGuest({ type: 'qemu', workloadType: 'vm', lastBackup: 0 }), diff --git a/frontend-modern/src/components/Workloads/guestRowModel.tsx b/frontend-modern/src/components/Workloads/guestRowModel.tsx index be5603386..05d6aa687 100644 --- a/frontend-modern/src/components/Workloads/guestRowModel.tsx +++ b/frontend-modern/src/components/Workloads/guestRowModel.tsx @@ -183,6 +183,7 @@ export const GUEST_COLUMNS: ColumnDef[] = [ width: '56px', minWidth: '48px', maxWidth: '64px', + toggleable: true, kind: 'badge', }, {