diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 971e11f167..ef219d852b 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,22 +1,16 @@ { "version": 1, - "base_sha": "91d8822d76c5a7214995c0e370be637b6e6336e3", - "verified_at": "2026-09-29T22:13:56Z", + "base_sha": "c3e8b6dd6dff36b5396ba2eb147d4e58d23001a6", + "verified_at": "2026-09-29T23:04:22Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/shared/ContainerUpdateBadge.tsx", - "frontend-modern/src/components/shared/containerUpdateBadgeModel.ts", - "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts", - "frontend-modern/src/stores/containerUpdates.ts" + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx" ], "content_sha256": { - "frontend-modern/src/components/shared/ContainerUpdateBadge.tsx": "fd88f3e3e275926572126f43cce7296e7361b5e527868dd3c59c7eb951e9f5c9", - "frontend-modern/src/components/shared/containerUpdateBadgeModel.ts": "10bc488cb82ae84c6ae46d77ec672d34df2a1b3c28e138ae0f3b4214cccbe37e", - "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts": "4b9dd4a42549b0433a1afa56f26183fe721c80626f823fb3c8489934721267c5", - "frontend-modern/src/stores/containerUpdates.ts": "b5d47132f7daf7f14bcc38462043288fc211344410af622ada7728694bcd514d" + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "7172690587f8e642ee30492481abbe76ee79d277aad290c3b50310dd1c0b282b" }, "routes": [ - "/browser-tests/update-action-receipt.html (production Docker update button with scripted action reads)" + "/pbs-correlation-proof.html (production PBS backup-server table and resource drawer with scripted, source-scoped resources)" ], "viewports": [ { @@ -29,28 +23,79 @@ } ], "states": [ - "governed update receipt pending after registry updateAvailable becomes false", - "existing action re-opened by GET with no second plan or execute", - "recorded completed action after a second GET changes the displayed outcome" + "three PBS services with distinct corroborated Agent links, including two PVE guests and one standalone host", + "host rows omitted in a transient refresh while the backend link is unchanged", + "third backend link withdrawn while the Agent row remains omitted" ], "interactions": [ - "inspect pending button and absence of a Current or Completed claim", - "select Review action and inspect receipt-pending dialog", - "select Check for receipt and inspect recorded completion", - "assert both action requests are GET; no page errors or horizontal overflow" + "open all three PBS drawers and inspect their Metrics Target identity", + "omit host rows and confirm targets stay on the correlated hosts", + "withdraw the third link and confirm its drawer returns to agent:pbs-three, with no page errors or horizontal overflow" ], - "command": "pulse-worker-browser frontend-modern/browser-tests/update-action-receipt.cjs from assigned workspace root; Vite 6.4.3, Chromium 141.0.7390.37, Playwright 1.56.1", + "command": "pulse-worker-browser pbs-browser-proof.cjs from assigned workspace root; Vite 6.4.3, Playwright 1.56.1, Chromium desktop/phone", "artifacts": [ - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-browser-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/pending-1365.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/review-1365.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/completed-1365.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/pending-390.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/review-390.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/completed-390.png" + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof.cjs", + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof-artifacts/pbs-1365-linked.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof-artifacts/pbs-1365-withdrawn.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof-artifacts/pbs-390-linked.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-sfkzldum/pbs-browser-proof-artifacts/pbs-390-withdrawn.png" ], - "notes": "Mock-backed UI proof; six screenshots inspected, including pending and completed phone states. This does not establish installed behaviour or resolve #1891.", + "notes": "Mock-backed proof of the real PBS table and drawer; all four screenshots inspected. It does not establish installed #1723 relief or observed Agent History series.", "prior_verifications": [ + { + "version": 1, + "base_sha": "91d8822d76c5a7214995c0e370be637b6e6336e3", + "verified_at": "2026-09-29T22:13:56Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/shared/ContainerUpdateBadge.tsx", + "frontend-modern/src/components/shared/containerUpdateBadgeModel.ts", + "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts", + "frontend-modern/src/stores/containerUpdates.ts" + ], + "content_sha256": { + "frontend-modern/src/components/shared/ContainerUpdateBadge.tsx": "fd88f3e3e275926572126f43cce7296e7361b5e527868dd3c59c7eb951e9f5c9", + "frontend-modern/src/components/shared/containerUpdateBadgeModel.ts": "10bc488cb82ae84c6ae46d77ec672d34df2a1b3c28e138ae0f3b4214cccbe37e", + "frontend-modern/src/components/shared/useContainerUpdateButtonState.ts": "4b9dd4a42549b0433a1afa56f26183fe721c80626f823fb3c8489934721267c5", + "frontend-modern/src/stores/containerUpdates.ts": "b5d47132f7daf7f14bcc38462043288fc211344410af622ada7728694bcd514d" + }, + "routes": [ + "/browser-tests/update-action-receipt.html (production Docker update button with scripted action reads)" + ], + "viewports": [ + { + "width": 1365, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "governed update receipt pending after registry updateAvailable becomes false", + "existing action re-opened by GET with no second plan or execute", + "recorded completed action after a second GET changes the displayed outcome" + ], + "interactions": [ + "inspect pending button and absence of a Current or Completed claim", + "select Review action and inspect receipt-pending dialog", + "select Check for receipt and inspect recorded completion", + "assert both action requests are GET; no page errors or horizontal overflow" + ], + "command": "pulse-worker-browser frontend-modern/browser-tests/update-action-receipt.cjs from assigned workspace root; Vite 6.4.3, Chromium 141.0.7390.37, Playwright 1.56.1", + "artifacts": [ + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-browser-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/pending-1365.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/review-1365.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/completed-1365.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/pending-390.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/review-390.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-ahrmjhme/update-action-receipt-proof-final/completed-390.png" + ], + "notes": "Mock-backed UI proof; six screenshots inspected, including pending and completed phone states. This does not establish installed behaviour or resolve #1891." + }, { "version": 1, "base_sha": "fbf17b271abe70ad3e12042506b52f85b6cbc484", diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx index 6a29b01ea5..8c696979d3 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx @@ -234,7 +234,12 @@ const hasCorrelationCandidate = (server: Resource, candidates: readonly Resource // only across such an omission, and only while the remembered host is still // plausibly current, so a genuinely removed or replaced host is not advertised // indefinitely. A host that is present but ambiguous still declines. -export type PbsCorrelationRetention = Map; +interface RetainedPbsCorrelation { + agent: Resource; + linkedAgentId?: string; +} + +export type PbsCorrelationRetention = Map; export const createPbsCorrelationRetention = (): PbsCorrelationRetention => new Map(); @@ -299,22 +304,26 @@ export function buildBackupServerRows( .map((server) => { const agent = uniquelyCorrelatedAgent(server, servers); if (agent) { - retention?.set(server.id, agent); + retention?.set(server.id, { + agent, + linkedAgentId: server.pbs?.linkedAgentId?.trim() || undefined, + }); return mergePBSAgentPresentation(server, agent); } const retained = retention?.get(server.id); if (retained) { - // A changed backend link is evidence of host replacement; an old - // correlation must not survive merely because its row is absent. - const linkedAgentId = server.pbs?.linkedAgentId?.trim(); - if (linkedAgentId && correlatedAgentKey(retained) !== linkedAgentId) { + // A changed or withdrawn backend link is evidence that the old host + // correlation is no longer trusted. A transiently missing row is not + // enough to keep advertising that host's History after revocation. + const linkedAgentId = server.pbs?.linkedAgentId?.trim() || undefined; + if (linkedAgentId !== retained.linkedAgentId) { retention?.delete(server.id); return server; } const fresh = - server.lastSeen - retained.lastSeen <= PBS_CORRELATION_RETENTION_MAX_STALENESS_MS; + server.lastSeen - retained.agent.lastSeen <= PBS_CORRELATION_RETENTION_MAX_STALENESS_MS; if (fresh && !hasCorrelationCandidate(server, servers)) { - return mergePBSAgentPresentation(server, retained); + return mergePBSAgentPresentation(server, retained.agent); } if (!fresh) retention?.delete(server.id); } diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts index 46d366a2e3..088026484b 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts @@ -146,6 +146,54 @@ const makeCorrelatedAgent = (overrides: Partial = {}): Resource => }) as Resource; describe('buildBackupServerRows PBS host correlation retention', () => { + it('keeps three PBS guests on their own History targets across PVE and VirtualBox hosts', () => { + const pbsServers = ['one', 'two', 'three'].map((suffix) => { + const pbs = makeCorrelatablePbs(); + pbs.id = `pbs-${suffix}`; + pbs.name = `backup-connection-${suffix}`; + pbs.pbs = { + ...pbs.pbs!, + instanceId: `pbs-${suffix}`, + hostname: `10.2.0.${suffix === 'one' ? 13 : suffix === 'two' ? 23 : 33}`, + nodeName: undefined, + linkedAgentId: `agent-${suffix}`, + }; + pbs.metricsTarget = { resourceType: 'agent', resourceId: `pbs-${suffix}` }; + return pbs; + }); + const agents = ['one', 'two', 'three'].map((suffix) => + makeCorrelatedAgent({ + id: `agent-${suffix}`, + name: `guest-${suffix}`, + agent: { agentId: `agent-${suffix}`, hostname: `guest-${suffix}` }, + metricsTarget: { resourceType: 'agent', resourceId: `agent-${suffix}` }, + }), + ); + const pveGuests = agents.slice(0, 2).map((agent): Resource => ({ + ...agent, + id: `vm-${agent.agent!.agentId}`, + type: 'vm', + sources: ['proxmox', 'agent'], + metricsTarget: { resourceType: 'vm', resourceId: `vm-${agent.agent!.agentId}` }, + })); + const otherAgents = Array.from({ length: 6 }, (_, index) => + makeCorrelatedAgent({ + id: `agent-unrelated-${index}`, + name: `backup-connection-${index % 3 === 0 ? 'one' : index % 3 === 1 ? 'two' : 'three'}`, + agent: { agentId: `agent-unrelated-${index}` }, + }), + ); + + const rows = buildBackupServerRows([...pbsServers, ...agents, ...pveGuests, ...otherAgents]); + expect( + rows.map((row) => [row.resource.id, row.resource.agent?.agentId, row.resource.metricsTarget]), + ).toEqual([ + ['pbs-one', 'agent-one', { resourceType: 'vm', resourceId: 'vm-agent-one' }], + ['pbs-three', 'agent-three', { resourceType: 'agent', resourceId: 'agent-three' }], + ['pbs-two', 'agent-two', { resourceType: 'vm', resourceId: 'vm-agent-two' }], + ]); + }); + it('keeps the agent UUID through PBS-only to side-by-side PVE/PBS refreshes without nodeName', () => { const retention = createPbsCorrelationRetention(); const pbs = makeCorrelatablePbs(); @@ -245,6 +293,22 @@ describe('buildBackupServerRows PBS host correlation retention', () => { expect(retention.size).toBe(0); }); + it('drops retained host History when a backend link is withdrawn', () => { + const retention = createPbsCorrelationRetention(); + const pbs = makeCorrelatablePbs(); + pbs.pbs = { ...pbs.pbs!, linkedAgentId: 'agent-proxback' }; + expect( + buildBackupServerRows([pbs, makeCorrelatedAgent()], [], retention)[0].resource.metricsTarget + ?.resourceId, + ).toBe('agent-proxback'); + + const withdrawn = { ...pbs, pbs: { ...pbs.pbs!, linkedAgentId: undefined } } as Resource; + expect( + buildBackupServerRows([withdrawn], [], retention)[0].resource.metricsTarget?.resourceId, + ).toBe('proxback'); + expect(retention.size).toBe(0); + }); + it('retains the resolved host target while a refresh snapshot omits the host row', () => { const retention = createPbsCorrelationRetention(); const pbs = makeCorrelatablePbs();