From 07d73843f09ad0d2eaedeb93ec408c6499edc8f4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 11 May 2026 15:59:43 +0100 Subject: [PATCH] Hide cluster deploy banner for offline PVE nodes The "N nodes unmonitored" banner gated purely on absence of a Pulse Unified Agent (r.agent?.agentId). Offline cluster members (status 'offline') were counted as deploy candidates, which is wrong on two fronts: those nodes are typically still covered by the cluster's PVE API token (so they aren't truly unmonitored, just unreachable), and an offline host is precisely the case where deploying an agent cannot succeed. Exclude status === 'offline' from the unmonitored count. --- .../Infrastructure/ClusterDeployBanner.tsx | 10 +++++-- .../__tests__/ClusterDeployBanner.test.tsx | 30 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx b/frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx index ccf3558a0..cf6b4121d 100644 --- a/frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx +++ b/frontend-modern/src/components/Infrastructure/ClusterDeployBanner.tsx @@ -17,7 +17,11 @@ export const ClusterDeployBanner: Component = (props) // 1. Non-empty cluster name (not Standalone) // 2. At least one resource has platformType === 'proxmox-pve' // 3. At least one resource has agent?.agentId (source agent exists) - // 4. At least one PVE node does NOT have an agent (unmonitored) + // 4. At least one *reachable* PVE node does NOT have an agent + // + // Offline nodes are excluded: we can't deploy a Pulse Unified Agent to + // a node we can't reach, and a node going temporarily offline is not + // the same situation as a never-onboarded cluster peer. const deployInfo = createMemo(() => { const resources = props.group.resources; const cluster = props.group.cluster; @@ -33,7 +37,9 @@ export const ClusterDeployBanner: Component = (props) const hasSourceAgent = pveNodes.some((r) => r.agent?.agentId); if (!hasSourceAgent) return null; - const unmonitoredCount = pveNodes.filter((r) => !r.agent?.agentId).length; + const unmonitoredCount = pveNodes.filter( + (r) => !r.agent?.agentId && r.status !== 'offline', + ).length; if (unmonitoredCount === 0) return null; return { cluster, unmonitoredCount }; diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ClusterDeployBanner.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ClusterDeployBanner.test.tsx index b7f28a6a7..2128e1593 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ClusterDeployBanner.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ClusterDeployBanner.test.tsx @@ -17,8 +17,12 @@ import type { Resource } from '@/types/resource'; /* ── helpers ──────────────────────────────────────────────────── */ -/** Minimal PVE node resource with optional agent */ -function makePveNode(id: string, agentId?: string): Resource { +/** Minimal PVE node resource with optional agent and overridable status */ +function makePveNode( + id: string, + agentId?: string, + status: Resource['status'] = 'online', +): Resource { return { id, type: 'agent', @@ -27,7 +31,7 @@ function makePveNode(id: string, agentId?: string): Resource { platformId: 'pve1', platformType: 'proxmox-pve', sourceType: 'api', - status: 'online', + status, agent: agentId ? { agentId } : undefined, } as Resource; } @@ -116,6 +120,26 @@ describe('ClusterDeployBanner', () => { expect(screen.getByText('1 node unmonitored')).toBeInTheDocument(); expect(screen.getByText('Review & Deploy')).toBeInTheDocument(); }); + + it('does not count offline no-agent nodes (cannot deploy to an unreachable host)', () => { + const group = makeGroup('my-cluster', [ + makePveNode('node1', 'agent-1'), // source agent + makePveNode('offline-node', undefined, 'offline'), // offline, no agent + ]); + render(() => ); + expect(screen.queryByText(/unmonitored/)).not.toBeInTheDocument(); + expect(screen.queryByText('Review & Deploy')).not.toBeInTheDocument(); + }); + + it('still counts online no-agent nodes when other no-agent nodes are offline', () => { + const group = makeGroup('my-cluster', [ + makePveNode('node1', 'agent-1'), + makePveNode('online-no-agent'), + makePveNode('offline-no-agent', undefined, 'offline'), + ]); + render(() => ); + expect(screen.getByText('1 node unmonitored')).toBeInTheDocument(); + }); }); describe('unmonitored count display', () => {