From dc66eb544ce7ad3ea8e64c208cda4ff315b846cf Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 5 Feb 2026 09:30:08 +0000 Subject: [PATCH] fix(config): ensure NotifyOnResolve defaults to true for new and legacy configs --- internal/api/notifications.go | 16 +++++++++++++++- internal/config/persistence.go | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/internal/api/notifications.go b/internal/api/notifications.go index 23cc3faf7..c39daf7a3 100644 --- a/internal/api/notifications.go +++ b/internal/api/notifications.go @@ -123,6 +123,14 @@ func (h *NotificationHandlers) UpdateEmailConfig(w http.ResponseWriter, r *http. log.Info(). Msg("Received email config update") + // Parse strict subset to check for presence of fields + var presenceCheck struct { + RateLimit *int `json:"rateLimit"` + } + if err := json.Unmarshal(body, &presenceCheck); err != nil { + // Non-fatal, just means we can't do presence check + } + var config notifications.EmailConfig if err := json.Unmarshal(body, &config); err != nil { log.Error().Err(err).Msg("Failed to parse email config") // Don't log body with passwords @@ -130,12 +138,18 @@ func (h *NotificationHandlers) UpdateEmailConfig(w http.ResponseWriter, r *http. return } + existingConfig := h.getMonitor(r.Context()).GetNotificationManager().GetEmailConfig() + // If password is empty, preserve the existing password if config.Password == "" { - existingConfig := h.getMonitor(r.Context()).GetNotificationManager().GetEmailConfig() config.Password = existingConfig.Password } + // If rateLimit was NOT provided (nil in presence check), preserve existing + if presenceCheck.RateLimit == nil { + config.RateLimit = existingConfig.RateLimit + } + log.Info(). Bool("enabled", config.Enabled). Str("smtp", config.SMTPHost). diff --git a/internal/config/persistence.go b/internal/config/persistence.go index 9fa0a97d5..c9349040f 100644 --- a/internal/config/persistence.go +++ b/internal/config/persistence.go @@ -530,6 +530,9 @@ func (c *ConfigPersistence) LoadAlertConfig() (*alerts.AlertConfig, error) { Memory: &alerts.HysteresisThreshold{Trigger: 85, Clear: 80}, Disk: &alerts.HysteresisThreshold{Trigger: 90, Clear: 85}, }, + Schedule: alerts.ScheduleConfig{ + NotifyOnResolve: true, + }, StorageDefault: alerts.HysteresisThreshold{Trigger: 85, Clear: 80}, TimeThreshold: 5, TimeThresholds: map[string]int{ @@ -594,6 +597,25 @@ func (c *ConfigPersistence) LoadAlertConfig() (*alerts.AlertConfig, error) { if config.HysteresisMargin <= 0 { config.HysteresisMargin = 5.0 } + // NotifyOnResolve defaults to true (send recovery notifications). + // Since bool zero-value is false, we check raw JSON to distinguish + // "missing field" (old configs) from "explicitly set to false". + if !config.Schedule.NotifyOnResolve { + // Use a temporary map to check if the field exists in the JSON + var raw map[string]interface{} + // Ignore error here as we've already unmarshaled successfully above + if json.Unmarshal(data, &raw) == nil { + // Check if "schedule" exists and has "notifyOnResolve" + if schedule, ok := raw["schedule"].(map[string]interface{}); ok { + if _, exists := schedule["notifyOnResolve"]; !exists { + config.Schedule.NotifyOnResolve = true + } + } else { + // "schedule" block missing entirely -> default to true + config.Schedule.NotifyOnResolve = true + } + } + } // NodeDefaults.Temperature: Allow Trigger=0 to disable temperature alerting if config.NodeDefaults.Temperature == nil || config.NodeDefaults.Temperature.Trigger < 0 { config.NodeDefaults.Temperature = &alerts.HysteresisThreshold{Trigger: 80, Clear: 75}