mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Filter Proxmox backups by repository
Change-source: pulse-maintainer
This commit is contained in:
parent
52a3373ae6
commit
95f57f446a
13 changed files with 229 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FilterSelectOption[]>(() => {
|
||||
const locations = new Map<string, string>();
|
||||
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<WorkloadCoverageRow[]>(() => {
|
||||
const selected = locationFilter();
|
||||
if (!selected) return recoveryModel().coverageRows;
|
||||
const newestArtifact = (artifacts: RecoverableArtifact[]): RecoverableArtifact | undefined =>
|
||||
artifacts.reduce<RecoverableArtifact | undefined>((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,
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<ProxmoxBackupsTable emptyIcon={<span />} 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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<RecoverableArtifact, 'locationKey' | 'locationLabel'> {
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ describe('resource link routing contract', () => {
|
|||
node: 'node',
|
||||
type: 'type',
|
||||
source: 'source',
|
||||
location: 'location',
|
||||
posture: 'posture',
|
||||
day: 'day',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const PROXMOX_BACKUPS_QUERY_PARAMS = {
|
|||
node: 'node',
|
||||
type: 'type',
|
||||
source: 'source',
|
||||
location: 'location',
|
||||
posture: 'posture',
|
||||
day: 'day',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue