From 2118e33459f8b4bb2b7a92a95833989d948ffe0a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 24 Jul 2026 21:15:49 +0100 Subject: [PATCH] Keep standalone agent machines out of the Virtualization Hosts thresholds list A machine monitored only by the Pulse agent was listed under Alerts, Thresholds, Virtualization Hosts as well as under Machines, because the section was fed by every resource of type `agent` rather than by provider-owned virtualization nodes. The two rows do not share an override identity. The Machines row resolves through `hostOverrideIdCandidates`, which prefers the agent-derived id, while the Virtualization Hosts row reads and writes `overridesMap.get(node.id)` on the bare resource id. A custom threshold saved from the Virtualization Hosts row therefore landed under a key agent alerting never reads, so the machine kept alerting on the global default with no visible reason. The section now takes `virtualizationHostResources`, which excludes standalone Pulse-agent resources via the existing `isPulseAgentPlatformResource` classifier already used by the standalone page, platform navigation and the resource drawer. Provider-owned nodes are unaffected, since they carry provider-owner platform evidence. Those machines lose nothing: they keep their Machines row, which is the one alerting honours. Reported via support by a customer whose cloud host kept firing at the 85% global agent memory limit despite a 100% override saved on the wrong row. --- .../alerts/AlertsConfigurationSurface.tsx | 2 +- .../__tests__/useAlertOverridesState.test.tsx | 47 +++++++++++++++++++ .../features/alerts/useAlertOverridesState.ts | 15 +++++- .../alerts/useAlertsConfigurationState.ts | 1 + 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx b/frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx index 6623b84d80..7ad2096aea 100644 --- a/frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx +++ b/frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx @@ -88,7 +88,7 @@ export function AlertsConfigurationSurface(props: AlertsConfigurationSurfaceProp pbsInstances={state.pbsInstances()} pmgInstances={state.pmgInstances()} containerRuntimes={state.containerRuntimeResources()} - nodes={props.byType('agent')} + nodes={state.virtualizationHostResources()} agents={state.agentResources()} storage={props .allResources() diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertOverridesState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertOverridesState.test.tsx index 0b6b3bfce4..0fb67d62b0 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertOverridesState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertOverridesState.test.tsx @@ -62,6 +62,53 @@ describe('useAlertOverridesState', () => { expect(overviewOverrides()).toEqual(result.overrides()); }); + it('keeps standalone agent machines out of the virtualization host list while retaining provider-owned nodes', () => { + const [hasUnsavedChanges] = createSignal(false); + const [, setOverviewOverrides] = createSignal([]); + const resources = [ + // A cloud box running only the Pulse agent. It belongs to the Machines tab, + // whose overrides resolve by agent identity, so a second row here would save + // an override that agent alerting never reads. + makeResource({ + id: 'docker-host-gcp-01', + name: 'docker-host-gcp-01', + type: 'agent', + platformType: 'agent', + sourceType: 'agent', + platformData: { + agent: { agentId: 'agent-gcp-01', platform: 'Linux' }, + }, + }), + // A real Proxmox node must still appear under Virtualization Hosts. + makeResource({ + id: 'cluster-a:pve01', + name: 'pve01', + type: 'agent', + platformType: 'proxmox-pve', + sourceType: 'api', + platformData: { + proxmox: { node: 'pve01', instance: 'cluster-a' }, + }, + }), + ]; + + const { result } = renderHook(() => + useAlertOverridesState({ + allResources: () => resources, + byType: (resourceType) => resources.filter((resource) => resource.type === resourceType), + children: () => [], + hasUnsavedChanges, + setOverviewOverrides, + }), + ); + + expect(result.virtualizationHostResources().map((resource) => resource.id)).toEqual([ + 'cluster-a:pve01', + ]); + // The standalone machine is still reachable, via the Machines tab. + expect(result.agentResources().map((resource) => resource.id)).toContain('docker-host-gcp-01'); + }); + it('projects guest overrides without agent resources and clears stale overrides when config is emptied', async () => { const [hasUnsavedChanges] = createSignal(false); const [overviewOverrides, setOverviewOverrides] = createSignal([]); diff --git a/frontend-modern/src/features/alerts/useAlertOverridesState.ts b/frontend-modern/src/features/alerts/useAlertOverridesState.ts index c727c7a253..10bbaa901b 100644 --- a/frontend-modern/src/features/alerts/useAlertOverridesState.ts +++ b/frontend-modern/src/features/alerts/useAlertOverridesState.ts @@ -3,7 +3,10 @@ import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js' import type { PBSInstance, PMGInstance } from '@/types/api'; import type { RawOverrideConfig } from '@/types/alerts'; import type { Resource, ResourceType } from '@/types/resource'; -import { isAgentFacetInfrastructureResource } from '@/utils/agentResources'; +import { + isAgentFacetInfrastructureResource, + isPulseAgentPlatformResource, +} from '@/utils/agentResources'; import { pbsInstanceFromResource, pmgInstanceFromResource } from '@/utils/resourceStateAdapters'; import { @@ -48,6 +51,15 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) { props.allResources().filter((resource) => isAgentFacetInfrastructureResource(resource)), ); + // Standalone Pulse-agent machines are not virtualization hosts. They own a row + // in the Machines tab, which resolves overrides by agent identity. Leaving them + // in the Virtualization Hosts list gave the same machine a second row that saved + // overrides under the bare resource id, an identity agent alerting never reads, + // so the override silently did nothing. + const virtualizationHostResources = createMemo(() => + props.byType('agent').filter((resource) => !isPulseAgentPlatformResource(resource)), + ); + const pbsInstances = createMemo(() => props .allResources() @@ -157,6 +169,7 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) { replaceRawOverridesConfig, allGuests, agentResources, + virtualizationHostResources, containerRuntimeResources, pbsInstances, pmgInstances, diff --git a/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts b/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts index b8a9cb79fc..4210ef1eaa 100644 --- a/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts +++ b/frontend-modern/src/features/alerts/useAlertsConfigurationState.ts @@ -140,6 +140,7 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro ...configurationSnapshotState, allGuests: overridesState.allGuests, agentResources: overridesState.agentResources, + virtualizationHostResources: overridesState.virtualizationHostResources, containerRuntimeResources: overridesState.containerRuntimeResources, pbsInstances: overridesState.pbsInstances, pmgInstances: overridesState.pmgInstances,