mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user