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',
},
{