From 4ac224084e983c40de7c4b551972bd078fd588f9 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 30 Jun 2026 16:56:35 +0100 Subject: [PATCH] Guard Proxmox Patrol coverage before hydration --- .../features/proxmox/ProxmoxPageSurface.tsx | 24 +++++++++++-------- .../ProxmoxPageSurface.contract.test.tsx | 19 +++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index 6be618848..b001b7200 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -84,6 +84,7 @@ const PROXMOX_WORKLOAD_STATUS_OPTIONS: readonly WorkloadsStatusOption[] = [ const ProxmoxIcon = getPlatformIcon('proxmox'); const proxmoxIcon = () => ; +const EMPTY_PROXMOX_PAGE_MODEL = buildProxmoxPageModel([]); interface MonitorPatrolContext { latestRun: PatrolRunRecord | null; @@ -111,7 +112,9 @@ export function ProxmoxPageSurface() { cacheKey: 'proxmox-workspace', initialHydration: 'prefer-ws-then-rest', }); - const model = createMemo(() => buildProxmoxPageModel(resources())); + const model = createMemo(() => + buildProxmoxPageModel(Array.isArray(resources()) ? resources() : []), + ); // Replication jobs come straight from /api/replication/jobs (they bypass // the unified-resource pipeline), so the surface owns the fetch: the job // count gates the Replication tab and the same data feeds the table. @@ -287,6 +290,7 @@ interface ProxmoxOverviewProps { } function ProxmoxOverview(props: ProxmoxOverviewProps) { + const currentModel = createMemo(() => props.model?.() ?? EMPTY_PROXMOX_PAGE_MODEL); const workloadsState = useWorkloadsState({ vms: [], containers: [], @@ -315,8 +319,8 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { ); const filteredNodes = createMemo(() => filterProxmoxNodesForSearch( - props.model().pveNodes, - props.model().guests, + currentModel().pveNodes, + currentModel().guests, workloadsState.search(), ), ); @@ -327,7 +331,7 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { getMonitorContextPatrolProtectionPosture({ findingCount: activePatrolFindings().length, latestRun: props.patrolContext()?.latestRun ?? null, - monitoredResourceCount: props.model().resources.length, + monitoredResourceCount: currentModel().resources.length, patrolStatus: props.patrolContext()?.status ?? null, pendingApprovalCount: aiIntelligenceStore.patrolPendingApprovalCount, workTypeComposition: getPatrolWorkTypeComposition(activePatrolFindings()), @@ -399,7 +403,7 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { } @@ -423,26 +427,26 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { /> {/* v5 dashboard closed with a running/stopped totals strip; the counts are platform-wide, matching the status-chip vocabulary above. */} - 0}> + 0}>
- {props.model().summary.runningGuestCount} running + {currentModel().summary.runningGuestCount} running - 0}> + 0}> - {props.model().summary.degradedGuestCount} attention + {currentModel().summary.degradedGuestCount} attention - {props.model().summary.stoppedGuestCount} stopped + {currentModel().summary.stoppedGuestCount} stopped
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx index 8ab8564d3..0e3525ab2 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx @@ -43,6 +43,15 @@ const setResources = (resources: Resource[]) => { }); }; +const setResourcesSnapshot = (resources: Resource[] | undefined, loading = false) => { + mockUseUnifiedResources.mockReturnValue({ + resources: () => resources as Resource[], + loading: () => loading, + error: () => null, + refetch: vi.fn(), + }); +}; + vi.mock('@/hooks/useUnifiedResources', () => ({ useUnifiedResources: (...args: unknown[]) => mockUseUnifiedResources(...args), })); @@ -266,6 +275,16 @@ describe('ProxmoxPageSurface contract', () => { ).not.toBeInTheDocument(); }); + it('does not crash when Patrol state refreshes before Proxmox resources hydrate', async () => { + setResourcesSnapshot(undefined, true); + + render(() => ); + + expect(screen.getByTestId('platform-table-loading-state')).toBeInTheDocument(); + await waitFor(() => expect(mockLoadPatrolFindings).toHaveBeenCalled()); + expect(screen.queryByRole('list', { name: 'Proxmox Patrol coverage' })).not.toBeInTheDocument(); + }); + it('does not render monitor-context Patrol coverage when active Patrol work exists', () => { mockPatrolState.findings = [{ status: 'active' }]; setResources([