From 252dec2c75853e62550dccac64e19b8e92de0fa5 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Mon, 11 Aug 2025 15:42:16 +0000 Subject: [PATCH] fix: improve alert threshold override persistence (addresses #295) Frontend improvements: - Store raw override config separately to handle delayed WebSocket state loading - Use createEffect to reprocess overrides when WebSocket state becomes available - Properly maintain raw config when adding/updating/removing overrides - Ensures overrides don't disappear when switching tabs or navigating This addresses the issue where custom thresholds would disappear after navigating away from the tab, which was caused by the WebSocket state not being fully loaded when the initial config was processed. --- frontend-modern/src/pages/Alerts.tsx | 102 ++++++++++++++++++--------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 088b84bb0..723979fc7 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -95,6 +95,7 @@ export function Alerts() { let scheduleRef: ScheduleRef = {}; const [overrides, setOverrides] = createSignal([]); + const [rawOverridesConfig, setRawOverridesConfig] = createSignal>({}); // Store raw config // Email configuration state moved to parent to persist across tab changes const [emailConfig, setEmailConfig] = createSignal({ @@ -131,6 +132,47 @@ export function Alerts() { } as EmailConfig; }; + // Process raw overrides config when state changes + createEffect(() => { + const rawConfig = rawOverridesConfig(); + if (Object.keys(rawConfig).length > 0 && state.nodes && state.vms && state.containers) { + // Convert overrides object to array format + const overridesList: Override[] = []; + + Object.entries(rawConfig).forEach(([key, thresholds]) => { + // Check if it's a node override by looking for matching node + const node = (state.nodes || []).find((n) => n.id === key); + if (node) { + overridesList.push({ + id: key, + name: node.name, + type: 'node', + resourceType: 'Node', + thresholds: extractTriggerValues(thresholds) + }); + } else { + // Find the guest by matching the full ID + const vm = (state.vms || []).find((g) => g.id === key); + const container = (state.containers || []).find((g) => g.id === key); + const guest = vm || container; + if (guest) { + overridesList.push({ + id: key, + name: guest.name, + type: 'guest', + resourceType: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + instance: guest.instance, + thresholds: extractTriggerValues(thresholds) + }); + } + } + }); + setOverrides(overridesList); + } + }); + // Load existing alert configuration on mount (only once) onMount(async () => { try { @@ -163,40 +205,8 @@ export function Alerts() { setTimeThreshold(config.timeThreshold); } if (config.overrides) { - // Convert overrides object to array format - const overridesList: Override[] = []; - - Object.entries(config.overrides).forEach(([key, thresholds]) => { - // Check if it's a node override by looking for matching node - const node = (state.nodes || []).find((n) => n.id === key); - if (node) { - overridesList.push({ - id: key, - name: node.name, - type: 'node', - resourceType: 'Node', - thresholds: extractTriggerValues(thresholds) - }); - } else { - // Find the guest by matching the full ID - const vm = (state.vms || []).find((g) => g.id === key); - const container = (state.containers || []).find((g) => g.id === key); - const guest = vm || container; - if (guest) { - overridesList.push({ - id: key, - name: guest.name, - type: 'guest', - resourceType: guest.type === 'qemu' ? 'VM' : 'CT', // Check type property to determine VM or CT - vmid: guest.vmid, - node: guest.node, - instance: guest.instance, - thresholds: extractTriggerValues(thresholds) - }); - } - } - }); - setOverrides(overridesList); + // Store raw config to be processed when state is available + setRawOverridesConfig(config.overrides); } // Pass schedule config to schedule tab if it exists if (config.schedule && scheduleRef.setScheduleConfig) { @@ -505,6 +515,8 @@ export function Alerts() { number; timeThreshold: () => number; overrides: () => Override[]; + rawOverridesConfig: () => Record; setGuestDefaults: (value: Record | ((prev: Record) => Record)) => void; setNodeDefaults: (value: Record | ((prev: Record) => Record)) => void; setStorageDefault: (value: number) => void; setTimeThreshold: (value: number) => void; setOverrides: (value: Override[]) => void; + setRawOverridesConfig: (value: Record) => void; activeAlerts: Record; setHasUnsavedChanges: (value: boolean) => void; } @@ -1321,10 +1335,22 @@ function ThresholdsTab(props: ThresholdsTabProps) { props.setOverrides(props.overrides().map((o: Override) => o.id === override.id ? updatedOverride : o )); + // Update raw config too + const newRawConfig = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: Record = {}; + Object.entries(updatedOverride.thresholds).forEach(([metric, value]) => { + hysteresisThresholds[metric] = { trigger: value, clear: Math.max(0, (value as number) - 5) }; + }); + newRawConfig[updatedOverride.id] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); props.setHasUnsavedChanges(true); }} onRemove={() => { props.setOverrides(props.overrides().filter((o) => o.id !== override.id)); + // Update raw config too + const newRawConfig = { ...props.rawOverridesConfig() }; + delete newRawConfig[override.id]; + props.setRawOverridesConfig(newRawConfig); props.setHasUnsavedChanges(true); }} /> @@ -1355,6 +1381,14 @@ function ThresholdsTab(props: ThresholdsTabProps) { existingOverrides={props.overrides()} onAdd={(override) => { props.setOverrides([...props.overrides(), override]); + // Update raw config too + const newRawConfig = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: Record = {}; + Object.entries(override.thresholds).forEach(([metric, value]) => { + hysteresisThresholds[metric] = { trigger: value, clear: Math.max(0, (value as number) - 5) }; + }); + newRawConfig[override.id] = hysteresisThresholds; + props.setRawOverridesConfig(newRawConfig); props.setHasUnsavedChanges(true); }} />