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
This commit is contained in:
pulse-triage[bot]
2026-08-29 09:24:55 +01:00
parent ea3f6388f3
commit bf3d11729b
9 changed files with 114 additions and 15 deletions
@@ -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
@@ -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
@@ -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
+17 -15
View File
@@ -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"
]
}
@@ -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();
});
@@ -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',
@@ -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),
@@ -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,
+15
View File
@@ -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`