From b4e1957c4747f0ceea2eb2b7e84b4fa2d7e86531 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 1 Jun 2026 11:35:57 +0100 Subject: [PATCH] Clarify Proxmox backup source labels --- .../features/proxmox/ProxmoxBackupsTable.tsx | 16 ++-- .../features/proxmox/ProxmoxCoverageTable.tsx | 45 ++++++---- .../__tests__/ProxmoxBackupsTable.test.tsx | 6 +- .../__tests__/ProxmoxCoverageTable.test.tsx | 4 +- .../proxmoxBackupRecoveryModel.test.ts | 6 ++ .../proxmoxBackupSourcePresentation.test.ts | 36 ++++++++ .../proxmoxBackupActivityPresentation.ts | 22 ++--- .../proxmox/proxmoxBackupRecoveryModel.ts | 37 +++++--- .../proxmoxBackupSourcePresentation.ts | 84 +++++++++++++++++++ .../proxmox/proxmoxBackupsTableShared.tsx | 43 +++++++--- 10 files changed, 243 insertions(+), 56 deletions(-) create mode 100644 frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts create mode 100644 frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx index 6022786c0..0e0d7848f 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx @@ -52,6 +52,7 @@ import { RECOVERABLE_FILTERS, artifactStateLabel, } from './proxmoxBackupsTableShared'; +import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation'; import { ProxmoxBackupsCoverageStrip } from './ProxmoxBackupsCoverageStrip'; import { ProxmoxBackupServersTable } from './ProxmoxBackupServersTable'; import { ProxmoxCoverageTable } from './ProxmoxCoverageTable'; @@ -200,7 +201,7 @@ export const ProxmoxBackupsTable: Component<{ ); // Node options for the scope filter: the distinct nodes across the workload - // set, "All nodes" first. PBS artifacts carry no node, so this scopes the + // set, "All nodes" first. PBS snapshots carry no node, so this scopes the // node-bearing rows only. const nodeOptions = createMemo(() => { const nodes = new Set(); @@ -297,7 +298,7 @@ export const ProxmoxBackupsTable: Component<{ }; }); - // Recoverable artifact feed: PBS, PVE archive, and guest snapshot rows. + // Recoverable artifact feed: PBS snapshots, PVE backup files, and guest snapshot rows. const RECOVERABLE_SEGMENT_KINDS: readonly BackupActivitySegmentKind[] = [ 'pbs', 'archive', @@ -462,7 +463,7 @@ export const ProxmoxBackupsTable: Component<{ } @@ -499,11 +500,12 @@ export const ProxmoxBackupsTable: Component<{ title="Backup health" tail={ - {liveTotalCount()} targets · {recoveryModel().coverageSummary.recoverableArtifacts}{' '} - restore points + {liveTotalCount()} targets ·{' '} + {recoveryModel().coverageSummary.recoverableArtifacts} restore points 0}> {' · '} - {recoveryModel().coverageSummary.withPBS} with PBS + {recoveryModel().coverageSummary.withPBS} with{' '} + {getProxmoxBackupSourcePresentation('pbs').filterLabel} 0}> {' · '} @@ -566,7 +568,7 @@ export const ProxmoxBackupsTable: Component<{ title={ recoverableMetricMode() === 'volume' ? 'Backup volume per day' : 'Backups per day' } - noun="artifact" + noun="backup" segmentKinds={RECOVERABLE_SEGMENT_KINDS} range={chartRange} onRangeChange={setChartRange} diff --git a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx index 27529497c..56a9f3d3d 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx @@ -28,6 +28,7 @@ import { type WorkloadCoverageRow, } from './proxmoxBackupRecoveryModel'; import type { CoverageSortKey } from './proxmoxBackupsTableModel'; +import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation'; import { ArtifactSourceBadge, ArtifactStateBadge, @@ -62,10 +63,11 @@ const taskWordVariant = (label: string): StatusIndicatorVariant => { }; // "Workload coverage" table: one row per workload answering "does this have a -// backup?" across PBS / archive / snapshot, each expanding to its restore -// evidence. Presentational — the parent owns the filtered+sorted memo, shared -// filters, and the expansion set. table-fixed + a colgroup keeps the columns -// from ballooning; the inner evidence table stays content-sized. +// backup?" across PBS snapshots / PVE backup files / guest snapshots, each +// expanding to its restore evidence. Presentational — the parent owns the +// filtered+sorted memo, shared filters, and the expansion set. table-fixed + a +// colgroup keeps the columns from ballooning; the inner evidence table stays +// content-sized. export function ProxmoxCoverageTable(props: { rows: WorkloadCoverageRow[]; hasAnyRows: boolean; @@ -78,7 +80,7 @@ export function ProxmoxCoverageTable(props: { expandedKeys: ReadonlySet; onToggleExpand: (key: string) => void; // Source columns auto-hide when no workload anywhere has that data (e.g. a - // PBS-only fleet drops the Archive and Snapshot columns), matching how the + // PBS-only fleet drops the PVE files and Snapshots columns), matching how the // source-detail tables already drop their conditional columns. showPbsColumn: boolean; showArchiveColumn: boolean; @@ -101,6 +103,9 @@ export function ProxmoxCoverageTable(props: { ]; const totalColumnWeight = () => visibleColumns().reduce((sum, c) => sum + c.weight, 0); const columnCount = () => visibleColumns().length; + const pbsSource = getProxmoxBackupSourcePresentation('pbs'); + const archiveSource = getProxmoxBackupSourcePresentation('archive'); + const snapshotSource = getProxmoxBackupSourcePresentation('snapshot'); return ( No restore point} > - {(artifact) => formatRelativeTime(artifact().createdAt, { compact: true })} + {(artifact) => + formatRelativeTime(artifact().createdAt, { compact: true }) + } @@ -293,7 +302,9 @@ export function ProxmoxCoverageTable(props: { > No PBS} + fallback={ + {pbsSource.coverageFallbackLabel} + } > {(artifact) => formatRelativeTime(artifact().createdAt, { compact: true }) @@ -307,7 +318,9 @@ export function ProxmoxCoverageTable(props: { > No archive} + fallback={ + {archiveSource.coverageFallbackLabel} + } > {(artifact) => formatRelativeTime(artifact().createdAt, { compact: true }) @@ -321,7 +334,9 @@ export function ProxmoxCoverageTable(props: { > No snapshot} + fallback={ + {snapshotSource.coverageFallbackLabel} + } > {(artifact) => formatRelativeTime(artifact().createdAt, { compact: true }) @@ -411,7 +426,9 @@ export function ProxmoxCoverageTable(props: { - {formatRelativeTime(artifact.createdAt, { compact: true })} + {formatRelativeTime(artifact.createdAt, { + compact: true, + })} { expect(screen.getByRole('columnheader', { name: /type/i })).toBeInTheDocument(); expect(screen.getAllByText('LXC').length).toBeGreaterThan(0); expect(screen.getAllByText('PBS').length).toBeGreaterThan(0); + 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(); + expect(screen.queryByRole('button', { name: 'Archives' })).not.toBeInTheDocument(); expect(screen.getByText('main / minipc')).toBeInTheDocument(); expect( screen.getByRole('cell', { @@ -193,7 +197,7 @@ describe('ProxmoxBackupsTable', () => { screen.getByRole('button', { name: /show restore evidence for pbs-docker/i }), ); expect(screen.getByText('Restore evidence')).toBeInTheDocument(); - expect(screen.getAllByText('PVE archive').length).toBeGreaterThan(0); + expect(screen.getAllByText('PVE file').length).toBeGreaterThan(0); expect(screen.getAllByText('Snapshot').length).toBeGreaterThan(0); }); diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx index dc5205e90..b4e13c405 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx @@ -59,8 +59,8 @@ describe('ProxmoxCoverageTable column visibility', () => { expect(headers).toContain('Node'); // Source columns gate on their flags. expect(headers).toContain('PBS'); - expect(headers).toContain('Snapshot'); - expect(headers).not.toContain('Archive'); + expect(headers).toContain('Snapshots'); + expect(headers).not.toContain('PVE files'); 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 65a7a1cff..72f493d77 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts @@ -103,7 +103,13 @@ describe('proxmoxBackupRecoveryModel', () => { expect(row.snapshotCount).toBe(1); expect(row.latestTask?.label).toBe('OK'); expect(getWorkloadRecoveryPostureLabel(row.posture)).toBe('Current'); + expect(model.recoverableArtifacts.map((artifact) => artifact.sourceLabel)).toEqual([ + 'PBS', + 'PVE file', + 'Snapshot', + ]); expect(coverageRowMatchesSearch(row, 'pbs-docker')).toBe(true); + expect(coverageRowMatchesSearch(row, 'PVE backup file')).toBe(true); expect(recoverableArtifactMatchesSearch(model.recoverableArtifacts[0], 'main')).toBe(true); }); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts new file mode 100644 index 000000000..06ce5e2e3 --- /dev/null +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupSourcePresentation.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; + +import { + PROXMOX_BACKUP_SOURCE_KINDS, + getProxmoxArchiveSourceTitle, + getProxmoxBackupSourcePresentation, +} from '../proxmoxBackupSourcePresentation'; + +describe('proxmoxBackupSourcePresentation', () => { + it('keeps Proxmox backup source vocabulary canonical across filters and badges', () => { + expect(PROXMOX_BACKUP_SOURCE_KINDS).toEqual(['pbs', 'archive', 'snapshot']); + + expect(getProxmoxBackupSourcePresentation('pbs')).toMatchObject({ + badgeLabel: 'PBS', + filterLabel: 'PBS snapshots', + timelineLabel: 'PBS snapshots', + }); + expect(getProxmoxBackupSourcePresentation('archive')).toMatchObject({ + badgeLabel: 'PVE file', + filterLabel: 'PVE backup files', + timelineLabel: 'PVE backup files', + }); + expect(getProxmoxBackupSourcePresentation('snapshot')).toMatchObject({ + badgeLabel: 'Snapshot', + filterLabel: 'Guest snapshots', + timelineLabel: 'Guest snapshots', + }); + }); + + it('explains PVE file rows without making them look like direct PBS rows', () => { + expect(getProxmoxArchiveSourceTitle(false)).toContain('Proxmox VE storage'); + expect(getProxmoxArchiveSourceTitle(true)).toBe( + 'Backup volume reported by Proxmox VE from a PBS-backed storage target', + ); + }); +}); diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupActivityPresentation.ts b/frontend-modern/src/features/proxmox/proxmoxBackupActivityPresentation.ts index c1b35e0ca..8524177ad 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupActivityPresentation.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupActivityPresentation.ts @@ -6,6 +6,8 @@ import { import { getRecoveryTimelineLabelEvery } from '@/utils/recoveryTimelineChartPresentation'; import { getRecoveryTimelineDayFilterStateLabel } from '@/utils/recoveryTimelinePresentation'; +import { getProxmoxBackupSourcePresentation } from './proxmoxBackupSourcePresentation'; + export type BackupActivityRangeDays = 7 | 30 | 90 | 365; export const BACKUP_ACTIVITY_RANGE_DAYS = [ @@ -31,14 +33,14 @@ interface BackupActivitySegmentPresentation { const SEGMENT_PRESENTATION: Record = { archive: { - label: 'Archives', - segmentClassName: 'bg-blue-500', - swatchClassName: 'bg-blue-500', + label: getProxmoxBackupSourcePresentation('archive').timelineLabel, + segmentClassName: getProxmoxBackupSourcePresentation('archive').timelineSegmentClassName, + swatchClassName: getProxmoxBackupSourcePresentation('archive').timelineSwatchClassName, }, pbs: { - label: 'PBS artifacts', - segmentClassName: 'bg-cyan-500', - swatchClassName: 'bg-cyan-500', + label: getProxmoxBackupSourcePresentation('pbs').timelineLabel, + segmentClassName: getProxmoxBackupSourcePresentation('pbs').timelineSegmentClassName, + swatchClassName: getProxmoxBackupSourcePresentation('pbs').timelineSwatchClassName, }, ok: { label: 'OK', @@ -56,9 +58,9 @@ const SEGMENT_PRESENTATION: Record { artifacts.push(artifact); if (!isCoverageWorkload(artifact.workload)) return; @@ -446,7 +456,8 @@ export function buildProxmoxBackupRecoveryModel( id: `pbs:${backup.id}`, nativeId: backup.id, sourceKind: 'pbs', - sourceLabel: 'PBS', + sourceLabel: pbsSource.badgeLabel, + sourceTitle: pbsSource.sourceTitle, workload, createdAt: backup.backupTime, createdMs, @@ -470,13 +481,14 @@ export function buildProxmoxBackupRecoveryModel( id: `archive:${archive.id}`, nativeId: archive.id, sourceKind: 'archive', - sourceLabel: archive.isPBS ? 'PVE PBS archive' : 'PVE archive', + sourceLabel: archiveSource.badgeLabel, + sourceTitle: getProxmoxArchiveSourceTitle(Boolean(archive.isPBS)), workload, createdAt: archive.time, createdMs, size: archive.size, location: archive.storage || archive.node || '—', - detail: archive.volid || archive.format || 'Backup archive', + detail: archive.volid || archive.format || archiveSource.detailFallbackLabel, protected: archive.protected, verified: archive.isPBS ? archive.verified : undefined, }); @@ -493,13 +505,14 @@ export function buildProxmoxBackupRecoveryModel( id: `snapshot:${snapshot.id}`, nativeId: snapshot.id, sourceKind: 'snapshot', - sourceLabel: 'Snapshot', + sourceLabel: snapshotSource.badgeLabel, + sourceTitle: snapshotSource.sourceTitle, workload, createdAt: snapshot.time, createdMs, size: snapshot.sizeBytes, location: snapshot.node || snapshot.instance || '—', - detail: snapshot.description || snapshot.name || 'Guest snapshot', + detail: snapshot.description || snapshot.name || snapshotSource.detailFallbackLabel, protected: false, }); } @@ -573,7 +586,8 @@ export function buildProxmoxBackupRecoveryModel( } export function coverageRowMatchesSearch(row: WorkloadCoverageRow, term: string): boolean { - if (!term) return true; + const normalizedTerm = term.trim().toLowerCase(); + if (!normalizedTerm) return true; const haystack = [ row.workload.label, row.workload.name, @@ -586,18 +600,20 @@ export function coverageRowMatchesSearch(row: WorkloadCoverageRow, term: string) row.latestTask?.error, ...row.artifacts.flatMap((artifact) => [ artifact.sourceLabel, + artifact.sourceTitle, artifact.location, artifact.detail, ]), ]; - return haystack.filter(Boolean).join(' ').toLowerCase().includes(term); + return haystack.filter(Boolean).join(' ').toLowerCase().includes(normalizedTerm); } export function recoverableArtifactMatchesSearch( artifact: RecoverableArtifact, term: string, ): boolean { - if (!term) return true; + const normalizedTerm = term.trim().toLowerCase(); + if (!normalizedTerm) return true; const haystack = [ artifact.workload.label, artifact.workload.name, @@ -606,6 +622,7 @@ export function recoverableArtifactMatchesSearch( artifact.workload.node, artifact.workload.instance, artifact.sourceLabel, + artifact.sourceTitle, artifact.location, artifact.detail, artifact.verified === true @@ -615,5 +632,5 @@ export function recoverableArtifactMatchesSearch( : undefined, artifact.protected ? 'protected' : 'unprotected', ]; - return haystack.filter(Boolean).join(' ').toLowerCase().includes(term); + return haystack.filter(Boolean).join(' ').toLowerCase().includes(normalizedTerm); } diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts b/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts new file mode 100644 index 000000000..01f1ce62d --- /dev/null +++ b/frontend-modern/src/features/proxmox/proxmoxBackupSourcePresentation.ts @@ -0,0 +1,84 @@ +export const PROXMOX_BACKUP_SOURCE_KINDS = ['pbs', 'archive', 'snapshot'] as const; + +export type ProxmoxBackupSourceKind = (typeof PROXMOX_BACKUP_SOURCE_KINDS)[number]; + +export interface ProxmoxBackupSourcePresentation { + badgeClassName: string; + badgeLabel: string; + compactFilterLabel: string; + coverageColumnLabel: string; + coverageFallbackLabel: string; + detailFallbackLabel: string; + filterAriaLabel: string; + filterLabel: string; + filterTitle: string; + sourceTitle: string; + stateFallbackLabel: string; + timelineLabel: string; + timelineSegmentClassName: string; + timelineSwatchClassName: string; +} + +const SOURCE_PRESENTATION: Record = { + pbs: { + badgeClassName: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200', + badgeLabel: 'PBS', + compactFilterLabel: 'PBS', + coverageColumnLabel: 'PBS', + coverageFallbackLabel: 'No PBS snapshot', + detailFallbackLabel: 'PBS snapshot', + filterAriaLabel: 'PBS snapshots from Proxmox Backup Server', + filterLabel: 'PBS snapshots', + filterTitle: 'Direct inventory from Proxmox Backup Server', + sourceTitle: 'Direct inventory from Proxmox Backup Server', + stateFallbackLabel: 'Snapshot', + timelineLabel: 'PBS snapshots', + timelineSegmentClassName: 'bg-cyan-500', + timelineSwatchClassName: 'bg-cyan-500', + }, + archive: { + badgeClassName: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200', + badgeLabel: 'PVE file', + compactFilterLabel: 'PVE files', + coverageColumnLabel: 'PVE files', + coverageFallbackLabel: 'No PVE backup file', + detailFallbackLabel: 'PVE backup file', + filterAriaLabel: 'PVE backup files found on Proxmox VE storage', + filterLabel: 'PVE backup files', + filterTitle: 'vzdump backup files or volumes reported by Proxmox VE storage', + sourceTitle: 'PVE backup files reported by Proxmox VE storage', + stateFallbackLabel: 'Backup file', + timelineLabel: 'PVE backup files', + timelineSegmentClassName: 'bg-blue-500', + timelineSwatchClassName: 'bg-blue-500', + }, + snapshot: { + badgeClassName: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200', + badgeLabel: 'Snapshot', + compactFilterLabel: 'Snapshots', + coverageColumnLabel: 'Snapshots', + coverageFallbackLabel: 'No guest snapshot', + detailFallbackLabel: 'Guest snapshot', + filterAriaLabel: 'Guest snapshots from Proxmox VE', + filterLabel: 'Guest snapshots', + filterTitle: 'Proxmox VE guest snapshots', + sourceTitle: 'Proxmox VE guest snapshot', + stateFallbackLabel: 'Snapshot', + timelineLabel: 'Guest snapshots', + timelineSegmentClassName: 'bg-violet-500', + timelineSwatchClassName: 'bg-violet-500', + }, +}; + +export function getProxmoxBackupSourcePresentation( + kind: ProxmoxBackupSourceKind, +): ProxmoxBackupSourcePresentation { + return SOURCE_PRESENTATION[kind]; +} + +export function getProxmoxArchiveSourceTitle(isPbsStorage: boolean): string { + if (isPbsStorage) { + return 'Backup volume reported by Proxmox VE from a PBS-backed storage target'; + } + return SOURCE_PRESENTATION.archive.sourceTitle; +} diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx b/frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx index 9eb613790..cfcf1fba2 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx +++ b/frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx @@ -8,6 +8,10 @@ import { TableHead } from '@/components/shared/Table'; import { WorkloadTypeBadge as SharedWorkloadTypeBadge } from '@/components/shared/WorkloadTypeBadge'; import type { RecoverableArtifact, WorkloadReference } from './proxmoxBackupRecoveryModel'; +import { + getProxmoxBackupSourcePresentation, + type ProxmoxBackupSourceKind, +} from './proxmoxBackupSourcePresentation'; import type { CoverageFilterValue, RecoverableFilterValue, @@ -56,11 +60,26 @@ export const COVERAGE_FILTERS: FilterOption[] = [ { value: 'uncovered', label: 'Uncovered', tone: 'danger', leading: statusDot('bg-red-500') }, ]; +const recoverableSourceFilterOption = ( + value: ProxmoxBackupSourceKind, +): FilterOption => { + const presentation = getProxmoxBackupSourcePresentation(value); + return { + value, + label: presentation.filterLabel, + ariaLabel: presentation.filterAriaLabel, + compactLabel: presentation.compactFilterLabel, + title: presentation.filterTitle, + tone: 'info', + leading: statusDot(presentation.timelineSwatchClassName), + }; +}; + export const RECOVERABLE_FILTERS: FilterOption[] = [ { value: 'all', label: 'All' }, - { value: 'pbs', label: 'PBS', tone: 'info', leading: statusDot('bg-cyan-500') }, - { value: 'archive', label: 'Archives', tone: 'info', leading: statusDot('bg-blue-500') }, - { value: 'snapshot', label: 'Snapshots', tone: 'info', leading: statusDot('bg-violet-500') }, + recoverableSourceFilterOption('pbs'), + recoverableSourceFilterOption('archive'), + recoverableSourceFilterOption('snapshot'), { value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') }, { value: 'unverified', label: 'Unverified', tone: 'warning', leading: statusDot('bg-amber-500') }, ]; @@ -121,11 +140,13 @@ export function RowMetricBar(props: { // Short state label for a recoverable artifact, paired with ArtifactStateBadge // for colour. Snapshot and protected take precedence over verification state. export function artifactStateLabel(artifact: RecoverableArtifact): string { - if (artifact.sourceKind === 'snapshot') return 'Snapshot'; + if (artifact.sourceKind === 'snapshot') { + return getProxmoxBackupSourcePresentation('snapshot').stateFallbackLabel; + } if (artifact.protected) return 'Protected'; if (artifact.verified === true) return 'Verified'; if (artifact.verified === false) return 'Unverified'; - return 'Archive'; + return getProxmoxBackupSourcePresentation(artifact.sourceKind).stateFallbackLabel; } export function ArtifactStateBadge(props: { artifact: RecoverableArtifact; label: string }) { @@ -161,15 +182,13 @@ export function ArtifactStateBadge(props: { artifact: RecoverableArtifact; label } export function ArtifactSourceBadge(props: { artifact: RecoverableArtifact }) { + const presentation = () => getProxmoxBackupSourcePresentation(props.artifact.sourceKind); + const title = () => props.artifact.sourceTitle ?? presentation().sourceTitle; + return ( {props.artifact.sourceLabel}