diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index fd9b80c6c..48cfab7e5 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -53,6 +53,8 @@ interface ThresholdsTableProps { setStorageDefault: (value: number) => void; timeThreshold: () => number; setTimeThreshold: (value: number) => void; + timeThresholds: () => { guest: number; node: number; storage: number; pbs: number }; + setTimeThresholds: (value: { guest: number; node: number; storage: number; pbs: number }) => void; setHasUnsavedChanges: (value: boolean) => void; activeAlerts?: Record; } @@ -793,29 +795,84 @@ export function ThresholdsTable(props: ThresholdsTableProps) { - {/* Additional settings row */} -
-
- - { - props.setTimeThreshold(parseInt(e.currentTarget.value) || 0); - props.setHasUnsavedChanges(true); - }} - class="w-14 px-1 py-0.5 text-xs border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> - - seconds before alerting - -
- + {/* Alert delay settings per resource type */} +
+
+

Alert Delay (seconds before triggering)

+
+
+ + { + props.setTimeThresholds({...props.timeThresholds(), guest: parseInt(e.currentTarget.value) || 0}); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+ + { + props.setTimeThresholds({...props.timeThresholds(), node: parseInt(e.currentTarget.value) || 0}); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+ + { + props.setTimeThresholds({...props.timeThresholds(), storage: parseInt(e.currentTarget.value) || 0}); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+ + { + props.setTimeThresholds({...props.timeThresholds(), pbs: parseInt(e.currentTarget.value) || 0}); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+
+ + {/* Reset button row */} +
+
{/* Search Bar */} diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index fc1f6edfb..52120e5d1 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -282,6 +282,23 @@ export function Alerts() { if (config.timeThreshold !== undefined) { setTimeThreshold(config.timeThreshold); } + // Load per-type time thresholds if available + if (config.timeThresholds) { + setTimeThresholds({ + guest: config.timeThresholds.guest ?? 10, + node: config.timeThresholds.node ?? 15, + storage: config.timeThresholds.storage ?? 30, + pbs: config.timeThresholds.pbs ?? 30 + }); + } else if (config.timeThreshold !== undefined) { + // Fallback to legacy single threshold for all types + setTimeThresholds({ + guest: config.timeThreshold, + node: config.timeThreshold, + storage: config.timeThreshold, + pbs: config.timeThreshold + }); + } if (config.overrides) { // Store raw config to be processed when state is available setRawOverridesConfig(config.overrides); @@ -461,7 +478,13 @@ export function Alerts() { }); const [storageDefault, setStorageDefault] = createSignal(85); - const [timeThreshold, setTimeThreshold] = createSignal(0); + const [timeThreshold, setTimeThreshold] = createSignal(0); // Legacy + const [timeThresholds, setTimeThresholds] = createSignal({ + guest: 10, + node: 15, + storage: 30, + pbs: 30 + }); const tabs: { id: AlertTab; label: string; icon: string }[] = [ { @@ -546,7 +569,8 @@ export function Alerts() { minimumDelta: 2.0, suppressionWindow: 5, hysteresisMargin: 5.0, - timeThreshold: timeThreshold() || 0, + timeThreshold: timeThreshold() || 0, // Legacy + timeThresholds: timeThresholds(), // Use rawOverridesConfig which is already properly formatted with disabled flags overrides: rawOverridesConfig(), schedule: { @@ -668,6 +692,8 @@ export function Alerts() { setStorageDefault={setStorageDefault} timeThreshold={timeThreshold} setTimeThreshold={setTimeThreshold} + timeThresholds={timeThresholds} + setTimeThresholds={setTimeThresholds} activeAlerts={activeAlerts} setHasUnsavedChanges={setHasUnsavedChanges} /> @@ -991,12 +1017,14 @@ interface ThresholdsTabProps { nodeDefaults: () => Record; storageDefault: () => number; timeThreshold: () => number; + timeThresholds: () => { guest: number; node: number; storage: number; pbs: 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; + setTimeThresholds: (value: { guest: number; node: number; storage: number; pbs: number }) => void; setOverrides: (value: Override[]) => void; setRawOverridesConfig: (value: Record) => void; activeAlerts: Record; @@ -1023,6 +1051,8 @@ function ThresholdsTab(props: ThresholdsTabProps) { setStorageDefault={props.setStorageDefault} timeThreshold={props.timeThreshold} setTimeThreshold={props.setTimeThreshold} + timeThresholds={props.timeThresholds} + setTimeThresholds={props.setTimeThresholds} setHasUnsavedChanges={props.setHasUnsavedChanges} activeAlerts={props.activeAlerts} /> diff --git a/frontend-modern/src/types/alerts.ts b/frontend-modern/src/types/alerts.ts index c8508faf8..938235c55 100644 --- a/frontend-modern/src/types/alerts.ts +++ b/frontend-modern/src/types/alerts.ts @@ -57,7 +57,13 @@ export interface AlertConfig { minimumDelta?: number; suppressionWindow?: number; hysteresisMargin?: number; - timeThreshold?: number; + timeThreshold?: number; // Legacy single global delay + timeThresholds?: { + guest?: number; + node?: number; + storage?: number; + pbs?: number; + }; aggregation?: { enabled: boolean; timeWindow: number; diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 0ea4dbe08..f675566f4 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -69,6 +69,7 @@ type ThresholdConfig struct { DiskWrite *HysteresisThreshold `json:"diskWrite,omitempty"` NetworkIn *HysteresisThreshold `json:"networkIn,omitempty"` NetworkOut *HysteresisThreshold `json:"networkOut,omitempty"` + Usage *HysteresisThreshold `json:"usage,omitempty"` // For storage devices // Legacy fields for backward compatibility CPULegacy *float64 `json:"cpuLegacy,omitempty"` MemoryLegacy *float64 `json:"memoryLegacy,omitempty"` @@ -169,7 +170,8 @@ type AlertConfig struct { MinimumDelta float64 `json:"minimumDelta"` // Minimum % change to trigger new alert SuppressionWindow int `json:"suppressionWindow"` // Minutes to suppress duplicate alerts HysteresisMargin float64 `json:"hysteresisMargin"` // Default margin for legacy thresholds - TimeThreshold int `json:"timeThreshold"` // Seconds that threshold must be exceeded before triggering + TimeThreshold int `json:"timeThreshold"` // Legacy: Seconds that threshold must be exceeded before triggering + TimeThresholds map[string]int `json:"timeThresholds"` // Per-type delays: guest, node, storage, pbs } // Manager handles alert monitoring and state @@ -922,6 +924,34 @@ func (m *Manager) clearAlert(alertID string) { } } +// getTimeThresholdForType returns the appropriate time threshold for the resource type +func (m *Manager) getTimeThresholdForType(resourceType string) int { + // Use per-type thresholds if available + if m.config.TimeThresholds != nil { + switch resourceType { + case "qemu", "lxc", "guest": + if delay, ok := m.config.TimeThresholds["guest"]; ok { + return delay + } + case "node": + if delay, ok := m.config.TimeThresholds["node"]; ok { + return delay + } + case "storage": + if delay, ok := m.config.TimeThresholds["storage"]; ok { + return delay + } + case "pbs": + if delay, ok := m.config.TimeThresholds["pbs"]; ok { + return delay + } + } + } + + // Fall back to legacy single threshold + return m.config.TimeThreshold +} + // checkMetric checks a single metric against its threshold with hysteresis func (m *Manager) checkMetric(resourceID, resourceName, node, instance, resourceType, metricType string, value float64, threshold *HysteresisThreshold) { if threshold == nil || threshold.Trigger <= 0 { @@ -956,24 +986,27 @@ func (m *Manager) checkMetric(resourceID, resourceName, node, instance, resource if value >= threshold.Trigger { // Threshold exceeded if !exists { + // Determine the appropriate time threshold based on resource type + timeThreshold := m.getTimeThresholdForType(resourceType) + // Check if we have a time threshold configured - if m.config.TimeThreshold > 0 { + if timeThreshold > 0 { // Check if this threshold was already pending if pendingTime, isPending := m.pendingAlerts[alertID]; isPending { // Check if enough time has passed - if time.Since(pendingTime) >= time.Duration(m.config.TimeThreshold)*time.Second { + if time.Since(pendingTime) >= time.Duration(timeThreshold)*time.Second { // Time threshold met, proceed with alert delete(m.pendingAlerts, alertID) log.Debug(). Str("alertID", alertID). - Int("timeThreshold", m.config.TimeThreshold). + Int("timeThreshold", timeThreshold). Dur("elapsed", time.Since(pendingTime)). Msg("Time threshold met, triggering alert") } else { // Still waiting for time threshold log.Debug(). Str("alertID", alertID). - Int("timeThreshold", m.config.TimeThreshold). + Int("timeThreshold", timeThreshold). Dur("elapsed", time.Since(pendingTime)). Msg("Threshold exceeded but waiting for time threshold") return @@ -983,7 +1016,7 @@ func (m *Manager) checkMetric(resourceID, resourceName, node, instance, resource m.pendingAlerts[alertID] = time.Now() log.Debug(). Str("alertID", alertID). - Int("timeThreshold", m.config.TimeThreshold). + Int("timeThreshold", timeThreshold). Msg("Threshold exceeded, starting time threshold tracking") return } diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 613760424..eb5db4e41 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -1175,7 +1175,7 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request // Check if there are overrides for this PBS node if alertConfig.Overrides != nil { - if override, exists := alertConfig.Overrides[monitoringID]; exists { + if _, exists := alertConfig.Overrides[monitoringID]; exists { log.Debug(). Str("nodeID", nodeID). Str("monitoringID", monitoringID). @@ -2855,7 +2855,7 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req host := r.Host // Dev environment fix: if we detect localhost from vite proxy AND we're in dev mode, use the actual IP - if host == "127.0.0.1:7656" { + if host == "127.0.0.1:7656" || host == "localhost:7656" { // Check if we're in development mode if _, err := os.Stat("/opt/pulse/.dev-mode"); err == nil { // This is the dev backend being proxied through vite on the dev machine