diff --git a/backend/internal/daemon/daemon.go b/backend/internal/daemon/daemon.go index 556fe5f09..fd426e7c0 100644 --- a/backend/internal/daemon/daemon.go +++ b/backend/internal/daemon/daemon.go @@ -61,6 +61,8 @@ func Run() error { return err } + notifier := startNotifier(ctx, cfg, store, log) + // Terminal streaming: the tmux runtime supplies the PTY-attach command and // liveness; the CDC broadcaster feeds the session-state channel. The manager // is handed to httpd, which mounts it at /mux. Raw PTY bytes never flow @@ -71,6 +73,11 @@ func Run() error { srv, err := httpd.New(cfg, log, termMgr) if err != nil { + stop() + notifier.Stop() + if cdcErr := cdcPipe.Stop(); cdcErr != nil { + log.Error("cdc pipeline shutdown", "err", cdcErr) + } return err } @@ -79,6 +86,11 @@ func Run() error { // trigger -> change_log -> poller -> broadcaster. lcStack, err := startLifecycle(ctx, store, log) if err != nil { + stop() + notifier.Stop() + if cdcErr := cdcPipe.Stop(); cdcErr != nil { + log.Error("cdc pipeline shutdown", "err", cdcErr) + } return err } @@ -98,6 +110,7 @@ func Run() error { // the LIFO trap (see comment after srv.Run), hence explicit. stop() lcStack.Stop() + notifier.Stop() if cdcErr := cdcPipe.Stop(); cdcErr != nil { log.Error("cdc pipeline shutdown", "err", cdcErr) } @@ -113,6 +126,7 @@ func Run() error { // runs before the cancel — which would hang any non-signal exit path. stop() lcStack.Stop() + notifier.Stop() if err := cdcPipe.Stop(); err != nil { log.Error("cdc pipeline shutdown", "err", err) } diff --git a/backend/notifier_wiring.go b/backend/internal/daemon/notifier_wiring.go similarity index 97% rename from backend/notifier_wiring.go rename to backend/internal/daemon/notifier_wiring.go index 5463859dd..5864b1e95 100644 --- a/backend/notifier_wiring.go +++ b/backend/internal/daemon/notifier_wiring.go @@ -1,4 +1,4 @@ -package main +package daemon import ( "context" diff --git a/backend/internal/notification/settings.go b/backend/internal/notification/settings.go index abb8b6531..457c5dc27 100644 --- a/backend/internal/notification/settings.go +++ b/backend/internal/notification/settings.go @@ -33,15 +33,9 @@ func NormalizeSettings(in config.NotificationConfig) config.NotificationConfig { def := config.DefaultNotificationConfig() out := in - if isZeroDashboardConfig(in.Dashboard) { - out.Dashboard.Enabled = def.Dashboard.Enabled - } if out.Dashboard.Limit == 0 { out.Dashboard.Limit = def.Dashboard.Limit } - if isZeroDesktopConfig(in.Desktop) { - out.Desktop.Enabled = def.Desktop.Enabled - } if out.Desktop.Priorities == nil { out.Desktop.Priorities = append([]ports.Priority(nil), def.Desktop.Priorities...) } @@ -93,23 +87,3 @@ func cloneRoutes(in map[ports.Priority][]string) map[ports.Priority][]string { } return out } - -func isZeroNotificationConfig(c config.NotificationConfig) bool { - return !c.Enabled && - isZeroDashboardConfig(c.Dashboard) && - isZeroDesktopConfig(c.Desktop) && - c.Routing.Priorities == nil && - c.Retry.MaxAttempts == 0 && - c.Retry.BaseDelay == 0 && - c.Retry.MaxDelay == 0 && - c.Retry.LeaseTTL == 0 && - c.Retry.BatchSize == 0 -} - -func isZeroDashboardConfig(c config.DashboardNotificationConfig) bool { - return !c.Enabled && c.Limit == 0 -} - -func isZeroDesktopConfig(c config.DesktopNotificationConfig) bool { - return !c.Enabled && len(c.Priorities) == 0 && len(c.SoundPriorities) == 0 -} diff --git a/backend/internal/notification/settings_test.go b/backend/internal/notification/settings_test.go index bd854e988..9fb9590f8 100644 --- a/backend/internal/notification/settings_test.go +++ b/backend/internal/notification/settings_test.go @@ -28,6 +28,27 @@ func TestSettingsFromConfigPreservesExplicitGlobalDisable(t *testing.T) { } } +func TestNormalizeSettingsPreservesExplicitSurfaceDisables(t *testing.T) { + got := StaticSettings(config.NotificationConfig{ + Enabled: true, + Dashboard: config.DashboardNotificationConfig{Enabled: false}, + Desktop: config.DesktopNotificationConfig{Enabled: false}, + }).Settings(context.Background()) + + if !got.Enabled { + t.Fatalf("global notifications should remain enabled: %+v", got) + } + if got.Dashboard.Enabled { + t.Fatalf("explicit dashboard disable should stay disabled: %+v", got.Dashboard) + } + if got.Desktop.Enabled { + t.Fatalf("explicit desktop disable should stay disabled: %+v", got.Desktop) + } + if got.Dashboard.Limit != 50 || len(got.Desktop.Priorities) == 0 || got.Retry.MaxAttempts != 5 { + t.Fatalf("disabled surfaces should still receive non-bool defaults: %+v", got) + } +} + func TestNormalizeSettingsPreservesExplicitEmptyRoute(t *testing.T) { cfg := config.DefaultNotificationConfig() cfg.Routing.Priorities[ports.PriorityUrgent] = []string{}