From bf3d11729b3174e38e8ed33f04147bd6e0236632 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:24:55 +0100 Subject: [PATCH] Preserve Proxmox guest state on first paint Keep canonical REST resource projection and owner-snapshot workload mapping aligned with realtime Proxmox backup and runtime evidence. This prevents the overview from showing transient missing backups and uptime before websocket hydration.\n\nRefs #1792 --- .../subsystems/performance-and-scalability.md | 10 ++++++ .../internal/subsystems/storage-recovery.md | 10 ++++++ .../internal/subsystems/unified-resources.md | 11 +++++++ frontend-modern/browser-verification.json | 32 +++++++++--------- .../__tests__/useUnifiedResources.test.ts | 13 ++++++++ .../src/hooks/__tests__/useWorkloads.test.ts | 4 +++ .../src/hooks/useUnifiedResources.ts | 33 +++++++++++++++++++ frontend-modern/src/hooks/useWorkloads.ts | 1 + frontend-modern/src/types/resource.ts | 15 +++++++++ 9 files changed, 114 insertions(+), 15 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 299d8a4ce..79c49cfb2 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1350,6 +1350,16 @@ still remove an authoritatively deleted guest. These rules preserve sort, selection, drawer, and virtualized viewport state without adding another resource scan, websocket subscription, or browser-local source of truth. +### Proxmox owner snapshots remain complete on first paint + +The scoped Proxmox owner reuses the canonical resource snapshot and performs +only constant-time field projection per workload row. That snapshot carries +top-level uptime and the intact source-authored Proxmox facet, so the first +successful REST paint can render runtime and backup evidence without showing +an interim unknown or never-backed-up state while waiting for a WebSocket +tick. This correction adds no request, subscription, retry, or resource scan; +`useUnifiedResources.test.ts` and `useWorkloads.test.ts` pin the boundary. + ### Command-session liveness lookup stays bounded and in-memory The connections ledger's command-channel liveness check diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 05a054e00..7e3616f18 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2389,6 +2389,16 @@ legacy `/proxmox/backups?view=...` query form remains compatibility input only; new links and tab state must use the path segment, preserve shared scope filters, and clear incompatible per-view facets when crossing sections. +### Overview backup evidence is authoritative on first paint + +The canonical REST-to-resource-to-workload boundary preserves Proxmox +`lastBackup` and `backupInProgress` evidence before the overview renders. A +missing value may be presented as never backed up only after that authoritative +facet is loaded; the UI must not require a later WebSocket merge to replace a +transport-induced absence. Preserving this read-only evidence adds no backup, +restore, retention, or recovery authority. The behavior is pinned by +`useUnifiedResources.test.ts` and `useWorkloads.test.ts`. + ### Retained Patrol objectives do not create recovery authority The shared `internal/api` retained-objective endpoints may scope an outcome to diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index c7e68d766..209c2ac9c 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2035,6 +2035,17 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and ## Current State +### Canonical REST facets preserve realtime workload evidence + +The frontend REST projection retains the complete source-authored Proxmox +facet, including runtime status, uptime, last backup, backup-in-progress, and +guest metadata, while applying only the canonical identity and metric +normalizations. It also preserves the provider-neutral `virtualMachine` facet. +Platform owners may therefore adapt the first REST snapshot into workload rows +without waiting for a later WebSocket merge; REST and WebSocket delivery must +not disagree about already-authoritative fields. The boundary is pinned by +`useUnifiedResources.test.ts` and `useWorkloads.test.ts`. + ### Agent libvirt domains use a provider-neutral VM facet The registry projects a host's validated libvirt domains as `vm` resources diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index d6c57cf47..ec22294a8 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,21 +1,23 @@ { "version": 1, - "base_sha": "0dc2c8c16d9612f981a29055cc703f6100cdf8cb", - "verified_at": "2026-08-28T19:44:59Z", + "base_sha": "ea3f6388f3eb00381b02f33a78bca403dafa6dd7", + "verified_at": "2026-08-29T08:24:25Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx" + "frontend-modern/src/hooks/useUnifiedResources.ts", + "frontend-modern/src/hooks/useWorkloads.ts", + "frontend-modern/src/types/resource.ts" ], "content_sha256": { - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "58e4c06e98108a69f1e62f631bb248eec626b3c6fc204e60507e377cd1633d8c", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "1e6daf19456ff79e7181e8117bbb2064feb53b852438294a58dd2fa5197b3817" + "frontend-modern/src/hooks/useUnifiedResources.ts": "9a30ba0035976f356abad5b9803bbddfdbafe28a77ef9d461c7ed3f8e70c7595", + "frontend-modern/src/hooks/useWorkloads.ts": "405e74ddd3cb490b54661fe6cd2d603adf47a56b1737f99c384f6a27cce2446c", + "frontend-modern/src/types/resource.ts": "0effa6ccaa032a106e9194d3f2c4665061aeb72abe31b062c0c8a5f002a7f5c1" }, "routes": ["/proxmox/overview", "/proxmox/backups/date"], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1600, + "height": 1000 }, { "width": 390, @@ -23,15 +25,15 @@ } ], "states": [ - "mock-backed Proxmox Overview with the bounded node preview flowing directly into Guests and no PBS server table", - "Proxmox Backups By date view retaining the PBS server and datastore table with artifact counts", - "expanded PBS server row retaining canonical resource details on the Backups tab", + "mock-backed Proxmox Overview first paint with completed, running, and genuinely absent backup states plus populated guest uptime", + "full-page Overview refresh retaining backup and uptime evidence without an interim unknown state", + "Overview revisited after navigating through the Proxmox backup workflow", "desktop and narrow layouts contained within the viewport without horizontal document overflow" ], "interactions": [ - "opened Proxmox Overview at 1280x800 and confirmed Nodes precede Guests with a 16 pixel section gap and no backup-server table", - "opened Proxmox Backups at 1280x800 and confirmed three PBS server/datastore rows remain visible", - "repeated Overview at 390x844 and confirmed Nodes and Guests remain visible without a duplicated PBS table or horizontal overflow", - "opened Proxmox Backups at 390x844, expanded the first PBS row, and confirmed canonical resource details remain usable without horizontal overflow" + "opened Proxmox Overview at 1600x1000 and confirmed 140 rendered backup indicators included completed, running, and no-backup states with no unknown uptime", + "refreshed Overview at 1600x1000 and confirmed the same 140 backup indicators and populated uptime remained on first paint", + "navigated to Proxmox Backups By date and back to Overview at 1600x1000, then confirmed backup and uptime evidence remained complete", + "repeated direct load, full-page refresh, and Backups-to-Overview route switching at 390x844; each state rendered 36 backup indicators, populated uptime, and no horizontal overflow" ] } diff --git a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts index 64160b2d7..b69041cba 100644 --- a/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts +++ b/frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts @@ -1169,6 +1169,7 @@ describe('useUnifiedResources', () => { type: 'vm', proxmox: { sourceId: 'site-a:pve1', + runtimeStatus: 'running', nodeName: 'pve1', clusterName: 'cluster-b', instance: 'site-a', @@ -1177,6 +1178,11 @@ describe('useUnifiedResources', () => { connectionHealth: 'error', pveVersion: '8.4.1', kernelVersion: '6.8.12-10-pve', + uptime: 86_400, + lastBackup: '2026-02-06T06:00:00Z', + backupInProgress: true, + osName: 'Debian GNU/Linux', + osVersion: '13', }, }, ], @@ -1204,7 +1210,14 @@ describe('useUnifiedResources', () => { connectionHealth: 'error', pveVersion: '8.4.1', kernelVersion: '6.8.12-10-pve', + runtimeStatus: 'running', + uptime: 86_400, + lastBackup: '2026-02-06T06:00:00Z', + backupInProgress: true, + osName: 'Debian GNU/Linux', + osVersion: '13', }); + expect(result!.resources()[0]?.uptime).toBe(86_400); dispose(); }); diff --git a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts index 631e6bb38..b5fe86706 100644 --- a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts +++ b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts @@ -249,7 +249,9 @@ describe('useWorkloads', () => { vmid: 101, nodeName: 'pve1', instance: 'cluster-a', + lastBackup: '2026-02-06T06:00:00Z', }, + uptime: 86_400, cpu: { current: 25 }, memory: { current: 50, used: 2 * 1024, total: 4 * 1024 }, disk: { current: 20, used: 20 * 1024, total: 100 * 1024 }, @@ -276,6 +278,8 @@ describe('useWorkloads', () => { vmid: 101, node: 'pve1', metricsTarget: { resourceType: 'vm', resourceId: 'mock-cluster-a-pve1-101' }, + uptime: 86_400, + lastBackup: Date.parse('2026-02-06T06:00:00Z'), alertResourceIds: expect.arrayContaining([ 'cluster-a-pve1-101', 'mock-cluster-a-pve1-101', diff --git a/frontend-modern/src/hooks/useUnifiedResources.ts b/frontend-modern/src/hooks/useUnifiedResources.ts index 2cdce5c46..67246b24c 100644 --- a/frontend-modern/src/hooks/useUnifiedResources.ts +++ b/frontend-modern/src/hooks/useUnifiedResources.ts @@ -27,6 +27,7 @@ import type { ResourceStorageRisk, ResourceTrueNASMeta, ResourceType, + ResourceVirtualMachineMeta, ResourceVMwareMeta, } from '@/types/resource'; import { normalizeDiskArray } from '@/utils/format'; @@ -226,8 +227,14 @@ type APIResource = { tags?: string[]; proxmox?: { sourceId?: string; + runtimeStatus?: string; + nodeIdentity?: string; nodeName?: string; + nodeAliases?: string[]; + nodeDisplayName?: string; + pool?: string; clusterName?: string; + isClusterMember?: boolean; instance?: string; host?: string; guestUrl?: string; @@ -237,18 +244,38 @@ type APIResource = { vmid?: number; cpus?: number; uptime?: number; + lastBackup?: string; + backupInProgress?: boolean; temperature?: number; temperatureDetails?: { available?: boolean; legacySensorsFormat?: boolean; }; template?: boolean; + containerType?: string; + isOci?: boolean; disks?: APIAgentDiskInfo[]; + diskStatusReason?: string; + guestAgentStatus?: string; + guestAgentExpected?: boolean; networkInterfaces?: APIAgentNetworkInterface[]; + osName?: string; + osVersion?: string; + agentVersion?: string; + osTemplate?: string; + hasDocker?: boolean; + dockerCheckedAt?: string; + loadAverage?: number[]; + pendingUpdates?: number; + temperatureMonitoringEnabled?: boolean; + pendingUpdatesCheckedAt?: string; swapUsed?: number; swapTotal?: number; balloon?: number; + memoryCache?: number; + lock?: string; }; + virtualMachine?: ResourceVirtualMachineMeta; agent?: { agentId?: string; agentVersion?: string; @@ -833,6 +860,11 @@ const toResource = (v2: APIResource): Resource => { ceph: v2.ceph as ResourceCephMeta | undefined, proxmox: v2.proxmox ? { + // Keep the canonical provider facet intact. The Proxmox overview + // adapts this Resource back into a workload row, so dropping fields + // here makes the first REST paint disagree with the later websocket + // merge (notably uptime and completed-backup state). + ...v2.proxmox, sourceId: v2.proxmox.sourceId, vmid: v2.proxmox.vmid, node: v2.proxmox.nodeName, @@ -854,6 +886,7 @@ const toResource = (v2: APIResource): Resource => { temperatureDetails: v2.proxmox.temperatureDetails, } : undefined, + virtualMachine: v2.virtualMachine, cpu: metricToResourceMetric(v2.metrics?.cpu), memory: metricToResourceMetric(v2.metrics?.memory), disk: metricToResourceMetric(v2.metrics?.disk), diff --git a/frontend-modern/src/hooks/useWorkloads.ts b/frontend-modern/src/hooks/useWorkloads.ts index cdf5caa27..84f89aa63 100644 --- a/frontend-modern/src/hooks/useWorkloads.ts +++ b/frontend-modern/src/hooks/useWorkloads.ts @@ -686,6 +686,7 @@ const mapCanonicalResourceToWorkload = (resource: Resource): WorkloadGuest | nul type: resource.type, name: resource.name, status: resource.status, + uptime: resource.uptime, lastSeen: resource.lastSeen ? new Date(resource.lastSeen).toISOString() : undefined, sources: resource.sources, platformScopes: resource.platformScopes, diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts index c8b64210d..00b946dac 100644 --- a/frontend-modern/src/types/resource.ts +++ b/frontend-modern/src/types/resource.ts @@ -715,14 +715,20 @@ export interface ResourceProxmoxMeta { pool?: string; instance?: string; clusterName?: string; + isClusterMember?: boolean; /** PVE API connection URL (json `host` on the backend ProxmoxData). */ host?: string; /** Operator-set web interface URL override. */ guestUrl?: string; /** Reachability of the Proxmox API source for this canonical node. */ connectionHealth?: string; + containerType?: string; cpus?: number; template?: boolean; + temperature?: number; + uptime?: number; + lastBackup?: string; + backupInProgress?: boolean; disks?: Disk[]; diskStatusReason?: string; guestAgentStatus?: string; @@ -738,6 +744,15 @@ export interface ResourceProxmoxMeta { pendingUpdates?: number; isOci?: boolean; osTemplate?: string; + osName?: string; + osVersion?: string; + agentVersion?: string; + hasDocker?: boolean; + dockerCheckedAt?: string; + loadAverage?: number[]; + temperatureMonitoringEnabled?: boolean; + pendingUpdatesCheckedAt?: string; + lock?: string; pveVersion?: string; kernelVersion?: string; // Narrow projection of the node's temperature payload. `legacySensorsFormat`