Fix alert disable notification suppression
Some checks failed
Build and Test / Secret Scan (push) Has been cancelled
Build and Test / Frontend & Backend (push) Has been cancelled
Core E2E Tests / Playwright Core E2E (push) Has been cancelled

This commit is contained in:
rcourtman 2026-03-07 18:40:08 +00:00
parent d6e8bffaeb
commit 0dd3fc779b
6 changed files with 140 additions and 7 deletions

View file

@ -1822,7 +1822,7 @@ export function Alerts() {
alertsActivation.config()?.observationWindowHours;
const alertConfig = {
enabled: true,
enabled: alertsActivation.config()?.enabled ?? true,
activationState: existingActivationState ?? undefined,
activationTime: existingActivationTime,
observationWindowHours: existingObservationWindowHours,

View file

@ -163,18 +163,20 @@ func (h *AlertHandlers) UpdateAlertConfig(w http.ResponseWriter, r *http.Request
updatedConfig := h.getMonitor(r.Context()).GetAlertManager().GetConfig()
// Update notification manager with schedule settings
h.getMonitor(r.Context()).GetNotificationManager().SetCooldown(updatedConfig.Schedule.Cooldown)
notificationMgr := h.getMonitor(r.Context()).GetNotificationManager()
notificationMgr.SetEnabled(updatedConfig.Enabled && updatedConfig.ActivationState == alerts.ActivationActive)
notificationMgr.SetCooldown(updatedConfig.Schedule.Cooldown)
groupWindow = updatedConfig.Schedule.Grouping.Window
if groupWindow == 0 && updatedConfig.Schedule.GroupingWindow != 0 {
groupWindow = updatedConfig.Schedule.GroupingWindow
}
h.getMonitor(r.Context()).GetNotificationManager().SetGroupingWindow(groupWindow)
h.getMonitor(r.Context()).GetNotificationManager().SetGroupingOptions(
notificationMgr.SetGroupingWindow(groupWindow)
notificationMgr.SetGroupingOptions(
updatedConfig.Schedule.Grouping.ByNode,
updatedConfig.Schedule.Grouping.ByGuest,
)
h.getMonitor(r.Context()).GetNotificationManager().SetNotifyOnResolve(updatedConfig.Schedule.NotifyOnResolve)
notificationMgr.SetNotifyOnResolve(updatedConfig.Schedule.NotifyOnResolve)
// Save to persistent storage
if err := h.getMonitor(r.Context()).GetConfigPersistence().SaveAlertConfig(updatedConfig); err != nil {
@ -215,6 +217,9 @@ func (h *AlertHandlers) ActivateAlerts(w http.ResponseWriter, r *http.Request) {
// Update config
h.getMonitor(r.Context()).GetAlertManager().UpdateConfig(config)
h.getMonitor(r.Context()).GetNotificationManager().SetEnabled(
config.Enabled && config.ActivationState == alerts.ActivationActive,
)
// Save to persistent storage
if err := h.getMonitor(r.Context()).GetConfigPersistence().SaveAlertConfig(config); err != nil {

View file

@ -143,14 +143,16 @@ func TestUpdateAlertConfig(t *testing.T) {
mockMonitor := new(MockAlertMonitor)
mockManager := new(MockAlertManager)
mockPersist := new(MockConfigPersistence)
notificationMgr := notifications.NewNotificationManager("")
defer notificationMgr.Stop()
mockMonitor.On("GetAlertManager").Return(mockManager)
mockMonitor.On("GetConfigPersistence").Return(mockPersist)
mockMonitor.On("GetNotificationManager").Return(&notifications.NotificationManager{})
mockMonitor.On("GetNotificationManager").Return(notificationMgr)
h := NewAlertHandlers(nil, mockMonitor, nil)
cfg := alerts.AlertConfig{Enabled: true}
cfg := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationPending}
mockManager.On("UpdateConfig", testifymock.Anything).Return()
mockManager.On("GetConfig").Return(cfg)
mockPersist.On("SaveAlertConfig", testifymock.Anything).Return(nil)
@ -162,6 +164,42 @@ func TestUpdateAlertConfig(t *testing.T) {
h.UpdateAlertConfig(w, req)
assert.Equal(t, 200, w.Code)
assert.False(t, notificationMgr.IsEnabled())
}
func TestActivateAlerts_EnablesNotificationManager(t *testing.T) {
mockMonitor := new(MockAlertMonitor)
mockManager := new(MockAlertManager)
mockPersist := new(MockConfigPersistence)
notificationMgr := notifications.NewNotificationManager("")
defer notificationMgr.Stop()
mockMonitor.On("GetAlertManager").Return(mockManager)
mockMonitor.On("GetConfigPersistence").Return(mockPersist)
mockMonitor.On("GetNotificationManager").Return(notificationMgr)
initialConfig := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationPending}
updatedConfig := alerts.AlertConfig{Enabled: true, ActivationState: alerts.ActivationActive}
mockManager.On("GetConfig").Return(initialConfig).Once()
mockManager.On("UpdateConfig", testifymock.AnythingOfType("alerts.AlertConfig")).Run(func(args testifymock.Arguments) {
cfg := args.Get(0).(alerts.AlertConfig)
if cfg.ActivationState != alerts.ActivationActive {
t.Fatalf("expected activation state to be active, got %q", cfg.ActivationState)
}
}).Return()
mockPersist.On("SaveAlertConfig", testifymock.Anything).Return(nil)
mockManager.On("GetActiveAlerts").Return([]alerts.Alert{}).Maybe()
mockManager.On("GetConfig").Return(updatedConfig)
h := NewAlertHandlers(nil, mockMonitor, nil)
req := httptest.NewRequest("POST", "/api/alerts/activate", nil)
w := httptest.NewRecorder()
h.ActivateAlerts(w, req)
assert.Equal(t, 200, w.Code)
assert.True(t, notificationMgr.IsEnabled())
}
func TestGetActiveAlerts(t *testing.T) {

View file

@ -4221,6 +4221,7 @@ func New(cfg *config.Config) (*Monitor, error) {
if alertConfig, err := m.configPersist.LoadAlertConfig(); err == nil {
m.alertManager.UpdateConfig(*alertConfig)
// Apply schedule settings to notification manager
m.notificationMgr.SetEnabled(alertConfig.Enabled && alertConfig.ActivationState == alerts.ActivationActive)
m.notificationMgr.SetCooldown(alertConfig.Schedule.Cooldown)
groupWindow := alertConfig.Schedule.Grouping.Window
if groupWindow == 0 && alertConfig.Schedule.GroupingWindow != 0 {

View file

@ -651,6 +651,51 @@ func (n *NotificationManager) GetQueue() *NotificationQueue {
return n.queue
}
// SetEnabled toggles notification delivery globally for this runtime instance.
func (n *NotificationManager) SetEnabled(enabled bool) {
var (
queue *NotificationQueue
changed bool
)
n.mu.Lock()
changed = n.enabled != enabled
n.enabled = enabled
if !enabled {
for i := range n.pendingAlerts {
n.pendingAlerts[i] = nil
}
n.pendingAlerts = n.pendingAlerts[:0]
if n.groupTimer != nil {
n.groupTimer.Stop()
n.groupTimer = nil
}
queue = n.queue
}
n.mu.Unlock()
if changed {
log.Info().Bool("enabled", enabled).Msg("Updated notification manager enabled state")
}
if !enabled && queue != nil {
if err := queue.CancelByTypes([]string{
"email", "email_resolved", "email_escalation",
"webhook", "webhook_resolved", "webhook_escalation",
"apprise", "apprise_resolved", "apprise_escalation",
}); err != nil {
log.Error().Err(err).Msg("Failed to cancel queued notifications after global disable")
}
}
}
// IsEnabled reports whether notification delivery is currently enabled.
func (n *NotificationManager) IsEnabled() bool {
n.mu.RLock()
defer n.mu.RUnlock()
return n.enabled
}
// SendAlert sends notifications for an alert
func (n *NotificationManager) SendAlert(alert *alerts.Alert) {
n.mu.Lock()
@ -3367,9 +3412,13 @@ func (n *NotificationManager) ProcessQueuedNotification(notif *QueuedNotificatio
func (n *NotificationManager) resolveQueuedEmailConfig() (EmailConfig, error) {
n.mu.RLock()
enabled := n.enabled
config := copyEmailConfig(n.emailConfig)
n.mu.RUnlock()
if !enabled {
return EmailConfig{}, fmt.Errorf("%w: notifications are disabled", ErrNotificationCancelled)
}
if !config.Enabled {
return EmailConfig{}, fmt.Errorf("%w: email notifications are disabled", ErrNotificationCancelled)
}
@ -3379,9 +3428,13 @@ func (n *NotificationManager) resolveQueuedEmailConfig() (EmailConfig, error) {
func (n *NotificationManager) resolveQueuedAppriseConfig() (AppriseConfig, error) {
n.mu.RLock()
enabled := n.enabled
config := copyAppriseConfig(n.appriseConfig)
n.mu.RUnlock()
if !enabled {
return AppriseConfig{}, fmt.Errorf("%w: notifications are disabled", ErrNotificationCancelled)
}
if !config.Enabled {
return AppriseConfig{}, fmt.Errorf("%w: Apprise notifications are disabled", ErrNotificationCancelled)
}
@ -3391,9 +3444,13 @@ func (n *NotificationManager) resolveQueuedAppriseConfig() (AppriseConfig, error
func (n *NotificationManager) resolveQueuedWebhookConfig(queued WebhookConfig) (WebhookConfig, error) {
n.mu.RLock()
enabled := n.enabled
webhooks := copyWebhookConfigs(n.webhooks)
n.mu.RUnlock()
if !enabled {
return WebhookConfig{}, fmt.Errorf("%w: notifications are disabled", ErrNotificationCancelled)
}
if queued.ID != "" {
for _, webhook := range webhooks {
if webhook.ID != queued.ID {

View file

@ -2954,6 +2954,38 @@ func TestProcessQueuedNotification_WebhookUsesCurrentConfig(t *testing.T) {
}
}
func TestProcessQueuedNotification_CancelledWhenNotificationsDisabled(t *testing.T) {
t.Setenv("PULSE_DATA_DIR", t.TempDir())
nm := NewNotificationManager("")
defer nm.Stop()
nm.SetEnabled(false)
currentWebhook := WebhookConfig{
ID: "wh-disabled",
Name: "disabled",
URL: "https://example.invalid/webhook",
Method: http.MethodPost,
Enabled: true,
}
nm.AddWebhook(currentWebhook)
configJSON, err := json.Marshal(currentWebhook)
if err != nil {
t.Fatalf("marshal queued webhook: %v", err)
}
err = nm.ProcessQueuedNotification(&QueuedNotification{
ID: "test-webhook-global-disabled",
Type: "webhook",
Config: configJSON,
Alerts: []*alerts.Alert{testQueuedAlert()},
})
if !errors.Is(err, ErrNotificationCancelled) {
t.Fatalf("expected queued webhook to be cancelled when notifications are globally disabled, got: %v", err)
}
}
func TestSetEmailConfig_DisableCancelsQueuedEmailNotifications(t *testing.T) {
t.Setenv("PULSE_DATA_DIR", t.TempDir())