diff --git a/internal/alert/rules.go b/internal/alert/rules.go index 3207d63..e55cfe7 100644 --- a/internal/alert/rules.go +++ b/internal/alert/rules.go @@ -68,6 +68,60 @@ func (e *Engine) raiseAndNotify(ctx context.Context, hostID, kind, severity, mes }) } +// Acknowledge updates the alert row and fans out alert.acknowledged to +// every enabled channel. Best-effort: store errors are logged but the +// dispatch still fires only when the store update succeeds. +func (e *Engine) Acknowledge(ctx context.Context, alertID, userID string, when time.Time) error { + if err := e.store.Acknowledge(ctx, alertID, userID, when); err != nil { + return err + } + a, lerr := e.store.GetAlert(ctx, alertID) + if lerr != nil || a == nil { + // Acknowledge already succeeded; dispatch is best-effort. + return nil //nolint:nilerr + } + p := alertPayload(ctx, e.store, notification.EventAcknowledged, a) + go e.hub.Dispatch(context.WithoutCancel(ctx), p) + return nil +} + +// Resolve marks the alert resolved and fans out alert.resolved. +func (e *Engine) Resolve(ctx context.Context, alertID string, when time.Time) error { + a, _ := e.store.GetAlert(ctx, alertID) + if err := e.store.Resolve(ctx, alertID, when); err != nil { + return err + } + if a == nil { + return nil + } + p := alertPayload(ctx, e.store, notification.EventResolved, a) + go e.hub.Dispatch(context.WithoutCancel(ctx), p) + return nil +} + +// alertPayload builds a Payload from a stored Alert, looking up the host +// name when HostID is set. +func alertPayload(ctx context.Context, st *store.Store, ev notification.Event, a *store.Alert) notification.Payload { + hostID, hostName := "", "" + if a.HostID != nil { + hostID = *a.HostID + hostName = hostID + if h, err := st.GetHost(ctx, hostID); err == nil && h != nil { + hostName = h.Name + } + } + return notification.Payload{ + Event: ev, + AlertID: a.ID, + Severity: a.Severity, + Kind: a.Kind, + HostID: hostID, + HostName: hostName, + Message: a.Message, + RaisedAt: a.CreatedAt, + } +} + // resolveAndNotify clears every open (or acknowledged) alert for // (host_id, kind) via store.AutoResolve, then fires alert.resolved // for each row that was actually open. Best-effort — errors are diff --git a/internal/server/http/ui_alerts.go b/internal/server/http/ui_alerts.go index 1a736b3..06c82fb 100644 --- a/internal/server/http/ui_alerts.go +++ b/internal/server/http/ui_alerts.go @@ -119,7 +119,13 @@ func (s *Server) handleUIAlertAcknowledge(w stdhttp.ResponseWriter, r *stdhttp.R stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest) return } - if err := s.deps.Store.Acknowledge(r.Context(), id, u.ID, time.Now().UTC()); err != nil { + var err error + if s.deps.AlertEngine != nil { + err = s.deps.AlertEngine.Acknowledge(r.Context(), id, u.ID, time.Now().UTC()) + } else { + err = s.deps.Store.Acknowledge(r.Context(), id, u.ID, time.Now().UTC()) + } + if err != nil { slog.Warn("ui alerts: ack", "err", err) } _ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{ @@ -147,7 +153,13 @@ func (s *Server) handleUIAlertResolve(w stdhttp.ResponseWriter, r *stdhttp.Reque stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest) return } - if err := s.deps.Store.Resolve(r.Context(), id, time.Now().UTC()); err != nil { + var err error + if s.deps.AlertEngine != nil { + err = s.deps.AlertEngine.Resolve(r.Context(), id, time.Now().UTC()) + } else { + err = s.deps.Store.Resolve(r.Context(), id, time.Now().UTC()) + } + if err != nil { slog.Warn("ui alerts: resolve", "err", err) } _ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{