diff --git a/internal/store/alerts.go b/internal/store/alerts.go index 42fb2d1..ef9036f 100644 --- a/internal/store/alerts.go +++ b/internal/store/alerts.go @@ -66,9 +66,32 @@ func (s *Store) RaiseOrTouch(ctx context.Context, hostID, kind, severity, messag if err := tx.Commit(); err != nil { return "", false, err } + _ = s.refreshHostOpenAlertCount(ctx, s.db, hostID) return id, true, nil } +// refreshHostOpenAlertCount recomputes hosts.open_alert_count from the +// alerts table for one host. Self-healing: idempotent and survives +// out-of-order edits. Best-effort — errors are returned but callers +// generally discard them since the projection is non-critical. +func (s *Store) refreshHostOpenAlertCount(ctx context.Context, exec interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) +}, hostID string, +) error { + if hostID == "" { + return nil + } + _, err := exec.ExecContext(ctx, + `UPDATE hosts SET open_alert_count = ( + SELECT COUNT(*) FROM alerts + WHERE host_id = ? AND resolved_at IS NULL + ) WHERE id = ?`, hostID, hostID) + if err != nil { + return fmt.Errorf("store: refresh open_alert_count: %w", err) + } + return nil +} + // Acknowledge sets acknowledged_at + acknowledged_by; does NOT set // resolved_at. Idempotent — re-acknowledging just refreshes the timestamp. func (s *Store) Acknowledge(ctx context.Context, id, userID string, when time.Time) error { @@ -89,6 +112,8 @@ func (s *Store) Acknowledge(ctx context.Context, id, userID string, when time.Ti // Resolve marks the alert resolved. Idempotent on already-resolved rows // (no-op). func (s *Store) Resolve(ctx context.Context, id string, when time.Time) error { + var hostID sql.NullString + _ = s.db.QueryRowContext(ctx, `SELECT host_id FROM alerts WHERE id = ?`, id).Scan(&hostID) _, err := s.db.ExecContext(ctx, `UPDATE alerts SET resolved_at = ? WHERE id = ? AND resolved_at IS NULL`, @@ -96,6 +121,9 @@ func (s *Store) Resolve(ctx context.Context, id string, when time.Time) error { if err != nil { return fmt.Errorf("store: resolve alert: %w", err) } + if hostID.Valid { + _ = s.refreshHostOpenAlertCount(ctx, s.db, hostID.String) + } return nil } @@ -110,6 +138,7 @@ func (s *Store) AutoResolve(ctx context.Context, hostID, kind string, when time. if err != nil { return fmt.Errorf("store: auto-resolve: %w", err) } + _ = s.refreshHostOpenAlertCount(ctx, s.db, hostID) return nil }