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', () => {