From 3e0c7be81c2ef7f48bfeedbb45ddb2f0dd7e4500 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 1 Jun 2026 11:59:48 +0100 Subject: [PATCH] Clarify Proxmox backup table columns --- .../features/proxmox/ProxmoxCoverageTable.tsx | 13 ++++++-- .../proxmox/ProxmoxRecoverableTable.tsx | 14 ++++++--- .../__tests__/ProxmoxBackupsTable.test.tsx | 2 ++ .../__tests__/ProxmoxCoverageTable.test.tsx | 8 ++--- .../proxmoxBackupRecoveryModel.test.ts | 17 ++++++++++ .../proxmoxBackupSourcePresentation.test.ts | 3 ++ .../proxmox/proxmoxBackupRecoveryModel.ts | 8 ++++- .../proxmoxBackupSourcePresentation.ts | 6 ++-- .../proxmox/proxmoxBackupsTableShared.tsx | 31 ++++++++++++------- 9 files changed, 74 insertions(+), 28 deletions(-) diff --git a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx index 56a9f3d3d..f54bb9892 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx @@ -32,6 +32,7 @@ import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresent import { ArtifactSourceBadge, ArtifactStateBadge, + PROXMOX_BACKUP_COLUMN_LABELS, ProxmoxBackupWorkloadTypeBadge, SortableHead, artifactStateLabel, @@ -149,7 +150,9 @@ export function ProxmoxCoverageTable(props: { headClass={getPlatformTableHeadClassForKind('name')} /> Type - ID + + {PROXMOX_BACKUP_COLUMN_LABELS.targetId} + Node Source Location - Created + + {PROXMOX_BACKUP_COLUMN_LABELS.created} + Size State - Details + + {PROXMOX_BACKUP_COLUMN_LABELS.details} + diff --git a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx index 497ac6c5a..8f7eb67bd 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx @@ -29,6 +29,7 @@ import type { RecoverableSortKey } from './proxmoxBackupsTableModel'; import { ArtifactSourceBadge, ArtifactStateBadge, + PROXMOX_BACKUP_COLUMN_LABELS, ProxmoxBackupWorkloadTypeBadge, RowMetricBar, SortableHead, @@ -187,7 +188,9 @@ export function ProxmoxRecoverableTable(props: { headClass={getPlatformTableHeadClassForKind('name')} /> Type - ID + + {PROXMOX_BACKUP_COLUMN_LABELS.targetId} + - Details + + {PROXMOX_BACKUP_COLUMN_LABELS.details} + @@ -250,8 +255,7 @@ export function ProxmoxRecoverableTable(props: { colspan={COLUMN_COUNT} class="border-t border-border bg-surface-alt px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-base-content" > - {group.label} - {' '} + {group.label}{' '} {group.items.length} {group.items.length === 1 ? 'backup' : 'backups'} diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx index ba37784dc..81f0ee82c 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx @@ -143,8 +143,10 @@ describe('ProxmoxBackupsTable', () => { expect(screen.getByRole('columnheader', { name: /location/i })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /source/i })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /type/i })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /target id/i })).toBeInTheDocument(); expect(screen.getAllByText('LXC').length).toBeGreaterThan(0); expect(screen.getAllByText('PBS').length).toBeGreaterThan(0); + expect(screen.getByText('2 PBS files')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /pbs snapshots/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /pve backup files/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /guest snapshots/i })).toBeInTheDocument(); diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx index b4e13c405..78d055a3d 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx @@ -55,12 +55,12 @@ describe('ProxmoxCoverageTable column visibility', () => { // Always-on columns. expect(headers).toContain('Workload'); expect(headers).toContain('Type'); - expect(headers).toContain('ID'); + expect(headers).toContain('Target ID'); expect(headers).toContain('Node'); // Source columns gate on their flags. - expect(headers).toContain('PBS'); - expect(headers).toContain('Snapshots'); - expect(headers).not.toContain('PVE files'); + expect(headers).toContain('Latest PBS'); + expect(headers).toContain('Latest snapshot'); + expect(headers).not.toContain('Latest PVE file'); expect(headers).not.toContain('Latest task'); }); }); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts index 72f493d77..8a5f73237 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts @@ -108,11 +108,28 @@ describe('proxmoxBackupRecoveryModel', () => { 'PVE file', 'Snapshot', ]); + expect(model.recoverableArtifacts[0].detail).toBe('2 PBS files'); expect(coverageRowMatchesSearch(row, 'pbs-docker')).toBe(true); expect(coverageRowMatchesSearch(row, 'PVE backup file')).toBe(true); expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'main')).toBe(true); }); + it('does not describe PBS backups with omitted file manifests as zero-file backups', () => { + const model = buildProxmoxBackupRecoveryModel({ + workloads: [workload({})], + pbsBackups: [pbsBackup({ files: [] })], + archives: [], + snapshots: [], + tasks: [], + nowMs: Date.parse('2026-05-26T08:00:00Z'), + }); + + expect(model.recoverableArtifacts[0].detail).toBe('PBS files not listed'); + expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'not listed')).toBe( + true, + ); + }); + it('surfaces a failed latest backup task as workload attention', () => { const model = buildProxmoxBackupRecoveryModel({ workloads: [workload({})], diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts index 06ce5e2e3..6f59f2188 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts @@ -12,16 +12,19 @@ describe('proxmoxBackupSourcePresentation', () => { expect(getProxmoxBackupSourcePresentation('pbs')).toMatchObject({ badgeLabel: 'PBS', + coverageColumnLabel: 'Latest PBS', filterLabel: 'PBS snapshots', timelineLabel: 'PBS snapshots', }); expect(getProxmoxBackupSourcePresentation('archive')).toMatchObject({ badgeLabel: 'PVE file', + coverageColumnLabel: 'Latest PVE file', filterLabel: 'PVE backup files', timelineLabel: 'PVE backup files', }); expect(getProxmoxBackupSourcePresentation('snapshot')).toMatchObject({ badgeLabel: 'Snapshot', + coverageColumnLabel: 'Latest snapshot', filterLabel: 'Guest snapshots', timelineLabel: 'Guest snapshots', }); diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts index 5cd8752fd..c01073bf7 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts @@ -116,6 +116,12 @@ function parseTimestampMs(value: string | undefined): number | undefined { return Number.isFinite(ms) ? ms : undefined; } +function pbsBackupFileDetailLabel(fileCount: number): string { + if (fileCount <= 0) return 'PBS files not listed'; + if (fileCount === 1) return '1 PBS file'; + return `${fileCount} PBS files`; +} + function normalizeKey(value: string | number | undefined | null): string { return String(value ?? '') .trim() @@ -463,7 +469,7 @@ export function buildProxmoxBackupRecoveryModel( createdMs, size: backup.size, location: `${backup.datastore || '—'} / ${backup.namespace?.trim() || '(root)'}`, - detail: backup.files.length === 1 ? '1 file' : `${backup.files.length} files`, + detail: pbsBackupFileDetailLabel(backup.files.length), protected: backup.protected, verified: backup.verified, fileCount: backup.files.length, diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts b/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts index 01f1ce62d..2e60b6349 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts @@ -24,7 +24,7 @@ const SOURCE_PRESENTATION: Record ; -export const ARCHIVE_STATUS_FILTERS: FilterOption<'all' | 'protected' | 'verified' | 'unverified'>[] = - [ - { value: 'all', label: 'All' }, - { value: 'protected', label: 'Protected', tone: 'info', leading: statusDot('bg-blue-500') }, - { value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') }, - { - value: 'unverified', - label: 'Unverified', - tone: 'warning', - leading: statusDot('bg-amber-500'), - }, - ]; +export const PROXMOX_BACKUP_COLUMN_LABELS = { + targetId: 'Target ID', + created: 'Created', + details: 'Details', +} as const; + +export const ARCHIVE_STATUS_FILTERS: FilterOption< + 'all' | 'protected' | 'verified' | 'unverified' +>[] = [ + { value: 'all', label: 'All' }, + { value: 'protected', label: 'Protected', tone: 'info', leading: statusDot('bg-blue-500') }, + { value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') }, + { + value: 'unverified', + label: 'Unverified', + tone: 'warning', + leading: statusDot('bg-amber-500'), + }, +]; export const PBS_STATUS_FILTERS: FilterOption<'all' | 'protected' | 'verified' | 'unverified'>[] = [ { value: 'all', label: 'All' },