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.
This commit is contained in:
rcourtman 2026-05-11 15:59:43 +01:00
parent 9329258f8b
commit 07d73843f0
2 changed files with 35 additions and 5 deletions

View file

@ -17,7 +17,11 @@ export const ClusterDeployBanner: Component<ClusterDeployBannerProps> = (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<ClusterDeployBannerProps> = (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 };

View file

@ -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(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
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(() => <ClusterDeployBanner group={group} onDeploy={vi.fn()} />);
expect(screen.getByText('1 node unmonitored')).toBeInTheDocument();
});
});
describe('unmonitored count display', () => {