From 95f57f446a39df98ad7b0d0a17f27b46070bcf9e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Aug 2026 06:44:50 +0100 Subject: [PATCH] Filter Proxmox backups by repository Change-source: pulse-maintainer --- .../subsystems/deployment-installability.md | 7 ++ .../subsystems/frontend-primitives.md | 8 ++ .../internal/subsystems/storage-recovery.md | 12 +++ .../internal/subsystems/unified-resources.md | 5 ++ docs/releases/RELEASE_NOTES_v6.4.0-rc.1.md | 4 +- frontend-modern/browser-verification.json | 40 +++++----- .../features/proxmox/ProxmoxBackupsTable.tsx | 73 ++++++++++++++++++- .../__tests__/ProxmoxBackupsTable.test.tsx | 64 +++++++++++++++- .../proxmoxBackupRecoveryModel.test.ts | 7 ++ .../proxmox/proxmoxBackupRecoveryModel.ts | 26 +++++++ .../routing/__tests__/resourceLinks.test.ts | 1 + frontend-modern/src/routing/resourceLinks.ts | 1 + .../render_release_body_test.py | 7 +- 13 files changed, 229 insertions(+), 26 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 7b0b4f74fc..fafd0ec216 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -19,6 +19,13 @@ Own server installation, deployment bootstrap behavior, provider-hosted MSP deployment artifacts, update planning, and server-side update execution surfaces. +Operator-facing prerelease notes may accumulate already-landed customer +outcomes before the next candidate is cut. Each added product claim must stay +in the existing customer-facing section and have exact packet proof in +`render_release_body_test.py`; a note about repository-scoped Proxmox backup +review must name both PBS server and datastore rather than implying that a +workload or node filter provides the same boundary. + Provider-hosted MSP deploy artifacts must package the provider control plane as a least-privilege Docker provisioner. The packaged compose/setup path must avoid whole-host and Docker-data read mounts, expose storage admission only through diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e33632f2e5..982f490fbc 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -51,6 +51,14 @@ the workload state, table, panel, and row contracts carry the selected basis and resolved parent-node data to the canonical memory bar, and the memory column header must expose the non-default Host basis after the control closes. +Feature-owned scope controls that use the shared filter rail must keep their +state in the owning route and use stable, domain-authored option identities. +The Proxmox Backups `Backup location` control composes the shared filter +catalog, reads PBS instance plus datastore identity from the recovery model, +and persists unchanged between the By date and Coverage views. Clearing the +shared filter rail must remove that route value along with the other active +facets; the feature must not replace the shared rail with a page-local select. + Large-estate platform pages must keep one canonical inventory snapshot for the initial read and explicit refresh path. The Proxmox overview owns its unified resource request and passes that snapshot into the shared workloads state; the diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index f038397bad..af2ba89013 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -484,6 +484,18 @@ The Proxmox backup view selector follows the same ownership split: storage/recovery owns the chronological-versus-coverage view values and backup semantics, while the visible segmented selector shell must compose the frontend-primitives `FilterButtonGroup`. +Backup-location filtering is repository-scoped rather than host-scoped. Its +identity combines source kind, provider instance, and datastore/storage so two +PBS instances with the same datastore name remain distinct and the workload's +PVE node never masquerades as backup ownership. The route query owns that scope +across the chronological and coverage views. Coverage rows must narrow their +artifact collection, latest evidence, and per-source counts before rendering; +they retain the server-owned canonical protection posture rather than deriving +a location-local posture from incomplete client evidence. The focused model and +rendered-route proofs in +`frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts` +and `frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx` +pin this boundary. Tenant report branding settings are adjacent tenant-local configuration, not a storage or recovery product state. `reportBranding` persisted in a tenant runtime's `system.json` should be preserved by the existing tenant data diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 5314ebbd9c..d90229f6e5 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2128,6 +2128,11 @@ in `frontend-modern/src/routing/resourceLinks.ts` now serialize query state only; the owning platform/runtime route supplies the pathname. New drawer or correlation surfaces must anchor on platform routes (or stay-in-place drawer expansion) instead of resurrecting the retired top-level paths. +The Proxmox Backups route-state vocabulary includes the opaque `location` +facet. Its value is owned by the backup recovery model, not by canonical +resource identity: PBS instance and datastore form the repository scope, and +the same value must survive the date/coverage route handoff so both views +apply one repository selection without inventing a top-level recovery route. Unified-resource drawers and Kubernetes drill-down controls may own resource timeline filter semantics, namespace choices, and destination routes, but their native select chrome must compose the frontend-primitives-owned `FormSelect`. diff --git a/docs/releases/RELEASE_NOTES_v6.4.0-rc.1.md b/docs/releases/RELEASE_NOTES_v6.4.0-rc.1.md index 68faa48006..c8d2ba0092 100644 --- a/docs/releases/RELEASE_NOTES_v6.4.0-rc.1.md +++ b/docs/releases/RELEASE_NOTES_v6.4.0-rc.1.md @@ -35,7 +35,9 @@ build path. longer modulepreloads every lazy route chunk, while integrity coverage stays enforced for dynamically loaded assets. - Proxmox backup views use canonical routes, compact healthy-state shields, and - recovery pagination derived from the normalized query limit. + recovery pagination derived from the normalized query limit. Backup-location + filters distinguish PBS servers and datastores so local and off-site restore + points can be reviewed independently. - Mobile and touch layouts keep native page scrolling and gestures, avoid hover tooltips, use consistent disclosure affordances, and retain reachable table actions at narrow widths. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index b72f79433a..21e16ff2e3 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,21 +1,22 @@ { "version": 1, - "base_sha": "8f7d5c2e98120adeb11502d897733a6947a6b479", - "verified_at": "2026-08-25T23:41:08Z", + "base_sha": "52a3373ae6cafaffcd97bb466c42216b896fe5ce", + "verified_at": "2026-08-26T05:42:38Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/api/charts.ts", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx", - "frontend-modern/src/components/Workloads/nodeDrawerModel.ts", - "frontend-modern/src/types/resource.ts" + "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx", + "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts", + "frontend-modern/src/routing/resourceLinks.ts" ], "content_sha256": { - "frontend-modern/src/api/charts.ts": "b50cbfac778a32cc6a0ad51f48bcd673e5fc4242729a6ea81fcada1b76058ff1", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx": "b64d2f349d951fd134294118334421f022aa8cf01fab7c0caed176d9140d6ed1", - "frontend-modern/src/components/Workloads/nodeDrawerModel.ts": "671738353bf22d5047743343b8e2f8ba7576870dc59e53843b52db131f11fb76", - "frontend-modern/src/types/resource.ts": "b13c44c92ef18da5a895b5378b8874c353d55b605c22ae02ca00cbff5d2f0eb9" + "frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx": "8f4b168bc6748f9318d04d67f3b2a614bdeb0e5c6d884d5aec309e1b7d202886", + "frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts": "5fb8dd36ee2144095e3ad979109dc750649c5818f36e9587caafaef1313be4ec", + "frontend-modern/src/routing/resourceLinks.ts": "cdd803824f883dab64654fcceb92da963bd18013eae3d1cf768880a3f77bb96b" }, - "routes": ["/proxmox/overview"], + "routes": [ + "/proxmox/backups/date", + "/proxmox/backups/coverage" + ], "viewports": [ { "width": 1280, @@ -27,16 +28,15 @@ } ], "states": [ - "collapsed and expanded Proxmox node rows in the loaded overview table", - "agent-backed node history with utilization, network I/O, disk I/O, and thermal groups", - "API-only PVE node history with utilization, network I/O, and thermal groups and no unsupported disk I/O group", - "24-hour history with populated series and collecting states for unavailable mock series" + "unfiltered 3018-point By date inventory with multiple PBS servers and datastores", + "By date narrowed to backup-vault / offsite-vault with unrelated primary-vault and PVE evidence absent", + "Coverage narrowed to backup-vault / primary-vault with 381 of 932 targets and location-scoped expanded restore evidence", + "mobile Coverage filter rail with the active backup-location chip, clear control, and filtered target count" ], "interactions": [ - "signed in to the current development build, expanded Analytics A, and opened its History tab", - "inspected the complete agent-backed chart row at 1280x800 and the stacked cards, scrolling, and thermals at 390x844", - "injected an API-only Analytics A websocket fixture into the committed frontend build and repeated the History interaction at both viewports", - "confirmed the API-only drawer requested resourceType=node with a canonical node ID and received HTTP 200", - "confirmed API-only history remained aligned and unclipped while omitting Disk I/O, and agent-backed history retained Disk I/O" + "opened the Backup location filter and selected backup-vault / offsite-vault on the current development build", + "confirmed the URL-owned location query appeared and the By date table retained offsite-vault evidence while removing primary-vault evidence", + "switched to Coverage and confirmed the location query persisted, the target count narrowed, and an expanded row showed only primary-vault restore evidence", + "repeated the filtered Coverage state at 390x844, expanded the mobile filter rail, checked placement and clipping, and removed the location chip" ] } diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx index ef64f08990..dfdf028f97 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx @@ -45,6 +45,7 @@ import { coverageRowMatchesSearch, recoverableArtifactMatchesSearch, type RecoverableArtifact, + type WorkloadCoverageRow, } from './proxmoxBackupRecoveryModel'; import { COVERAGE_SORT_DEFAULT_DIRECTION, @@ -184,6 +185,14 @@ export const ProxmoxBackupsTable: Component<{ return !selected || node === selected; }; + const locationFilter = (): string => queryValue(PROXMOX_BACKUPS_QUERY_PARAMS.location); + const setLocationFilter = (value: string): void => + setSearchParams({ [PROXMOX_BACKUPS_QUERY_PARAMS.location]: value || null }, { replace: true }); + const locationMatches = (artifact: RecoverableArtifact): boolean => { + const selected = locationFilter(); + return !selected || artifact.locationKey === selected; + }; + // Type scope normalizes vm/qemu and ct/lxc to a canonical token before // comparing, since the recovery model keeps PBS-native ct internally. const typeFilter = (): string => queryValue(PROXMOX_BACKUPS_QUERY_PARAMS.type); @@ -290,6 +299,21 @@ export const ProxmoxBackupsTable: Component<{ ]; }); + const locationOptions = createMemo(() => { + const locations = new Map(); + for (const artifact of recoveryModel().recoverableArtifacts) { + if (artifact.locationKey && artifact.locationLabel) { + locations.set(artifact.locationKey, artifact.locationLabel); + } + } + return [ + { value: '', label: 'All backup locations' }, + ...[...locations] + .sort((a, b) => a[1].localeCompare(b[1])) + .map(([value, label]) => ({ value, label })), + ]; + }); + // Source columns auto-hide when no workload anywhere has that data (computed // over the full set so columns don't flicker as filters change). A PBS-only // fleet drops the Archive and Snapshot columns. @@ -308,10 +332,45 @@ export const ProxmoxBackupsTable: Component<{ recoveryModel().coverageRows.some((row) => Boolean(row.latestTask)), ); + const coverageRowsForLocation = createMemo(() => { + const selected = locationFilter(); + if (!selected) return recoveryModel().coverageRows; + const newestArtifact = (artifacts: RecoverableArtifact[]): RecoverableArtifact | undefined => + artifacts.reduce((latest, artifact) => { + if (!latest) return artifact; + return (artifact.createdMs ?? Number.NEGATIVE_INFINITY) > + (latest.createdMs ?? Number.NEGATIVE_INFINITY) + ? artifact + : latest; + }, undefined); + + return recoveryModel().coverageRows.flatMap((row) => { + const artifacts = row.artifacts.filter(locationMatches); + if (artifacts.length === 0) return []; + return [ + { + ...row, + artifacts, + latestRecovery: newestArtifact(artifacts), + latestPBS: newestArtifact(artifacts.filter((artifact) => artifact.sourceKind === 'pbs')), + latestArchive: newestArtifact( + artifacts.filter((artifact) => artifact.sourceKind === 'archive'), + ), + latestSnapshot: newestArtifact( + artifacts.filter((artifact) => artifact.sourceKind === 'snapshot'), + ), + pbsCount: artifacts.filter((artifact) => artifact.sourceKind === 'pbs').length, + archiveCount: artifacts.filter((artifact) => artifact.sourceKind === 'archive').length, + snapshotCount: artifacts.filter((artifact) => artifact.sourceKind === 'snapshot').length, + }, + ]; + }); + }); + const filteredCoverageRows = createMemo(() => { const term = search().trim().toLowerCase(); const filter = coverageFilter(); - const list = recoveryModel().coverageRows.filter((row) => { + const list = coverageRowsForLocation().filter((row) => { if (!nodeMatches(row.workload.node)) return false; if (!typeMatches(row.workload.type)) return false; if (filter !== 'all' && row.posture !== filter) return false; @@ -432,6 +491,7 @@ export const ProxmoxBackupsTable: Component<{ const list = recoveryModel().recoverableArtifacts.filter((artifact) => { if (!nodeMatches(artifact.workload.node)) return false; if (!typeMatches(artifact.workload.type)) return false; + if (!locationMatches(artifact)) return false; if ( (filter === 'pbs' || filter === 'archive' || filter === 'snapshot') && artifact.sourceKind !== filter @@ -493,6 +553,15 @@ export const ProxmoxBackupsTable: Component<{ setValue: setNodeFilter, defaultValue: '', }, + { + id: 'location', + label: 'Backup location', + group: 'scope', + options: locationOptions, + value: locationFilter, + setValue: setLocationFilter, + defaultValue: '', + }, { id: 'type', label: 'Type', @@ -532,6 +601,7 @@ export const ProxmoxBackupsTable: Component<{ Boolean( search().trim() || nodeFilter() || + locationFilter() || typeFilter() || coverageFilter() !== 'all' || recoverableFilter() !== 'all' || @@ -543,6 +613,7 @@ export const ProxmoxBackupsTable: Component<{ [PROXMOX_BACKUPS_QUERY_PARAMS.query]: null, [PROXMOX_BACKUPS_QUERY_PARAMS.view]: null, [PROXMOX_BACKUPS_QUERY_PARAMS.node]: null, + [PROXMOX_BACKUPS_QUERY_PARAMS.location]: null, [PROXMOX_BACKUPS_QUERY_PARAMS.type]: null, [PROXMOX_BACKUPS_QUERY_PARAMS.source]: null, [PROXMOX_BACKUPS_QUERY_PARAMS.posture]: null, diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx index ea91ea9665..befc36135b 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx @@ -113,9 +113,12 @@ const pbsPayload = { meta: { totalBackups: 1 }, }; -function mockBackupAPIs(state: 'protected' | 'attention' = 'protected') { +function mockBackupAPIs( + state: 'protected' | 'attention' = 'protected', + pbsResponse: typeof pbsPayload = pbsPayload, +) { apiFetchMock.mockImplementation((url: string) => { - if (url === '/api/backups/pbs') return Promise.resolve(jsonResponse(pbsPayload)); + if (url === '/api/backups/pbs') return Promise.resolve(jsonResponse(pbsResponse)); if (url === '/api/backups/pve') return Promise.resolve(jsonResponse(pvePayload)); return Promise.resolve(jsonResponse({})); }); @@ -352,7 +355,7 @@ describe('ProxmoxBackupsTable', () => { window.history.replaceState( {}, '', - '/?view=date&q=pbs-docker&source=pbs&node=pve-a&type=ct&day=2026-05-25', + '/?view=date&q=pbs-docker&source=pbs&location=pbs%3Apbs-main%3Amain&node=pve-a&type=ct&day=2026-05-25', ); renderInRouter(() => ( @@ -367,6 +370,9 @@ describe('ProxmoxBackupsTable', () => { 'true', ); expect(screen.getByRole('button', { name: /clear date filter/i })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Backup location: pbs-main / main' }), + ).toBeInTheDocument(); await fireEvent.click(screen.getByRole('button', { name: /clear filters/i })); @@ -375,6 +381,7 @@ describe('ProxmoxBackupsTable', () => { expect(params.get('view')).toBeNull(); expect(params.get('q')).toBeNull(); expect(params.get('source')).toBeNull(); + expect(params.get('location')).toBeNull(); expect(params.get('node')).toBeNull(); expect(params.get('type')).toBeNull(); expect(params.get('day')).toBeNull(); @@ -382,6 +389,57 @@ describe('ProxmoxBackupsTable', () => { expect(screen.getByPlaceholderText(/search backups by workload/i)).toHaveValue(''); }); + it('filters restore points and coverage by PBS server and datastore', async () => { + const offsitePayload = { + ...pbsPayload, + data: { + backups: [ + ...pbsPayload.data.backups, + { + ...pbsPayload.data.backups[0], + id: 'pbs-offsite/offsite/minipc/ct/112/2026-05-24T01:34:25Z', + instance: 'pbs-offsite', + datastore: 'offsite', + backupTime: '2026-05-24T01:34:25Z', + }, + ], + }, + }; + mockBackupAPIs('protected', offsitePayload); + + renderInRouter(() => ( + } workloads={[workloadResource]} /> + )); + + await screen.findByText('offsite / minipc'); + const filterSelect = screen.getByRole('combobox', { name: 'Filter' }); + const offsiteOption = screen.getByRole('option', { + name: 'Backup location: pbs-offsite / offsite', + }) as HTMLOptionElement; + await fireEvent.change(filterSelect, { target: { value: offsiteOption.value } }); + + await waitFor(() => + expect(new URLSearchParams(window.location.search).get('location')).toBe( + 'pbs:pbs-offsite:offsite', + ), + ); + expect(screen.getByText('offsite / minipc')).toBeInTheDocument(); + expect(screen.queryByText('main / minipc')).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Backup location: pbs-offsite / offsite' }), + ).toBeInTheDocument(); + + await fireEvent.click(screen.getByRole('link', { name: /coverage/i })); + await waitFor(() => expect(window.location.pathname).toBe('/proxmox/backups/coverage')); + expect(new URLSearchParams(window.location.search).get('location')).toBe( + 'pbs:pbs-offsite:offsite', + ); + expect(screen.getAllByText('pbs-docker').length).toBeGreaterThan(0); + await fireEvent.click(screen.getByRole('button', { name: /expand details for pbs-docker/i })); + expect(screen.getByText('offsite / minipc')).toBeInTheDocument(); + expect(screen.queryByText('main / minipc')).not.toBeInTheDocument(); + }); + it('clears incompatible URL facets when switching backup views', async () => { mockBackupAPIs(); window.history.replaceState({}, '', '/?view=date&source=pbs&day=2026-05-25'); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts index 543d0fd54c..14c4f8fd5e 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxBackupRecoveryModel.test.ts @@ -133,6 +133,13 @@ describe('proxmoxBackupRecoveryModel', () => { 'PVE file', 'Snapshot', ]); + expect( + model.recoverableArtifacts.map((artifact) => [artifact.locationKey, artifact.locationLabel]), + ).toEqual([ + ['pbs:pbs-main:main', 'pbs-main / main'], + ['archive:homelab:local', 'homelab / local'], + ['snapshot:homelab:minipc', 'homelab / minipc'], + ]); expect(model.recoverableArtifacts[0].detail).toBe('2 PBS files'); expect(coverageRowMatchesSearch(row, 'pbs-docker')).toBe(true); expect(coverageRowMatchesSearch(row, 'PVE backup file')).toBe(true); diff --git a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts index 9ce1aab887..5c62bf976f 100644 --- a/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxBackupRecoveryModel.ts @@ -42,6 +42,10 @@ export interface RecoverableArtifact { createdMs?: number; size?: number; location: string; + /** Stable source/repository identity used by the backup-location filter. */ + locationKey?: string; + /** Operator-facing source/repository label used by the backup-location filter. */ + locationLabel?: string; detail: string; protected: boolean; verified?: boolean; @@ -150,6 +154,25 @@ function normalizeKey(value: string | number | undefined | null): string { .toLowerCase(); } +function artifactLocationScope( + sourceKind: RecoverableSourceKind, + instance: string | undefined, + repository: string | undefined, +): Pick { + const parts = [instance?.trim(), repository?.trim()].filter((part): part is string => + Boolean(part), + ); + const distinctParts = parts.filter( + (part, index) => + parts.findIndex((candidate) => normalizeKey(candidate) === normalizeKey(part)) === index, + ); + if (distinctParts.length === 0) return {}; + return { + locationKey: [sourceKind, ...distinctParts.map(normalizeKey)].join(':'), + locationLabel: distinctParts.join(' / '), + }; +} + function isZeroWorkloadId(vmid: string): boolean { return normalizeKey(vmid) === '0'; } @@ -511,6 +534,7 @@ export function buildProxmoxBackupRecoveryModel( createdMs, size: backup.size, location: `${backup.datastore || '—'} / ${backup.namespace?.trim() || '(root)'}`, + ...artifactLocationScope('pbs', backup.instance, backup.datastore), detail: pbsBackupFileDetailLabel(backup.files.length), protected: backup.protected, verified: backup.inProgress ? undefined : backup.verified, @@ -537,6 +561,7 @@ export function buildProxmoxBackupRecoveryModel( createdMs, size: archive.size, location: archive.storage || archive.node || '—', + ...artifactLocationScope('archive', archive.instance, archive.storage || archive.node), detail: archive.volid || archive.format || archiveSource.detailFallbackLabel, protected: archive.protected, verified: archive.inProgress ? undefined : archive.isPBS ? archive.verified : undefined, @@ -562,6 +587,7 @@ export function buildProxmoxBackupRecoveryModel( createdMs, size: snapshot.sizeBytes, location: snapshot.node || snapshot.instance || '—', + ...artifactLocationScope('snapshot', snapshot.instance, snapshot.node), detail: snapshot.description || snapshot.name || snapshotSource.detailFallbackLabel, protected: false, }); diff --git a/frontend-modern/src/routing/__tests__/resourceLinks.test.ts b/frontend-modern/src/routing/__tests__/resourceLinks.test.ts index 07560651e1..8d617ae217 100644 --- a/frontend-modern/src/routing/__tests__/resourceLinks.test.ts +++ b/frontend-modern/src/routing/__tests__/resourceLinks.test.ts @@ -173,6 +173,7 @@ describe('resource link routing contract', () => { node: 'node', type: 'type', source: 'source', + location: 'location', posture: 'posture', day: 'day', }); diff --git a/frontend-modern/src/routing/resourceLinks.ts b/frontend-modern/src/routing/resourceLinks.ts index 43e813eafc..53066be67a 100644 --- a/frontend-modern/src/routing/resourceLinks.ts +++ b/frontend-modern/src/routing/resourceLinks.ts @@ -35,6 +35,7 @@ export const PROXMOX_BACKUPS_QUERY_PARAMS = { node: 'node', type: 'type', source: 'source', + location: 'location', posture: 'posture', day: 'day', } as const; diff --git a/scripts/release_control/render_release_body_test.py b/scripts/release_control/render_release_body_test.py index edc5d2ce70..b237a1a012 100644 --- a/scripts/release_control/render_release_body_test.py +++ b/scripts/release_control/render_release_body_test.py @@ -224,7 +224,7 @@ No manual migration is required. render_release_body._requires_customer_facing_standard("6.4.0") ) - def test_v640_rc1_packet_records_stale_container_health_fix(self) -> None: + def test_v640_rc1_packet_records_current_customer_fixes(self) -> None: notes = ( _REPO_ROOT / "docs" / "releases" / "RELEASE_NOTES_v6.4.0-rc.1.md" ).read_text(encoding="utf-8") @@ -236,6 +236,11 @@ No manual migration is required. "from the stale health-check result retained by the container runtime.", normalized_notes, ) + self.assertIn( + "Backup-location filters distinguish PBS servers and datastores so " + "local and off-site restore points can be reviewed independently.", + normalized_notes, + ) def test_future_release_notes_reject_internal_release_control_sections(self) -> None: notes = """# Pulse v6.4.0-rc.2 Release Notes