package http import ( "encoding/json" "log/slog" stdhttp "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/oklog/ulid/v2" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) type alertsPage struct { Filter store.AlertFilter Alerts []store.Alert Counts alertCounts HostNames map[string]string // host_id → name for table rendering } type alertCounts struct { Open int Acknowledged int Resolved24h int } // handleUIAlerts renders the alerts page with the chosen filters. func (s *Server) handleUIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) { u := s.requireUIUser(w, r) if u == nil { return } q := r.URL.Query() f := store.AlertFilter{ Status: q.Get("status"), Severity: q.Get("severity"), HostID: q.Get("host_id"), Search: strings.TrimSpace(q.Get("q")), Limit: 200, } if f.Status == "" { f.Status = "open" } alerts, err := s.deps.Store.ListAlerts(r.Context(), f) if err != nil { slog.Error("ui alerts: list", "err", err) stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) return } page := alertsPage{Filter: f, Alerts: alerts, HostNames: map[string]string{}} if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil { for _, h := range hosts { page.HostNames[h.ID] = h.Name } } page.Counts = computeAlertCounts(s, r) view := s.baseView(r, u) view.Title = "Alerts · restic-manager" view.Active = "alerts" view.Page = page if err := s.deps.UI.Render(w, "alerts", view); err != nil { slog.Error("ui alerts: render", "err", err) } } func computeAlertCounts(s *Server, r *stdhttp.Request) alertCounts { open, _ := s.deps.Store.ListAlerts(r.Context(), store.AlertFilter{Status: "open"}) acked, _ := s.deps.Store.ListAlerts(r.Context(), store.AlertFilter{Status: "acknowledged"}) cutoff := time.Now().UTC().Add(-24 * time.Hour) all, _ := s.deps.Store.ListAlerts(r.Context(), store.AlertFilter{Status: "resolved"}) res := 0 for _, a := range all { if a.ResolvedAt != nil && a.ResolvedAt.After(cutoff) { res++ } } return alertCounts{Open: len(open), Acknowledged: len(acked), Resolved24h: res} } // handleAPIAlerts is the JSON list — same filter shape. func (s *Server) handleAPIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) { if _, ok := s.requireUser(r); !ok { writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "") return } q := r.URL.Query() f := store.AlertFilter{ Status: q.Get("status"), Severity: q.Get("severity"), HostID: q.Get("host_id"), Search: strings.TrimSpace(q.Get("q")), Limit: 200, } alerts, err := s.deps.Store.ListAlerts(r.Context(), f) if err != nil { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "") return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(alerts) } // handleUIAlertAcknowledge is POST /alerts/{id}/acknowledge. func (s *Server) handleUIAlertAcknowledge(w stdhttp.ResponseWriter, r *stdhttp.Request) { u := s.requireUIUser(w, r) if u == nil { return } id := chi.URLParam(r, "id") if id == "" { stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest) return } if err := s.deps.Store.Acknowledge(r.Context(), id, u.ID, time.Now().UTC()); err != nil { slog.Warn("ui alerts: ack", "err", err) } _ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{ ID: ulid.Make().String(), UserID: &u.ID, Actor: "user", Action: "alert.acknowledge", TargetKind: ptr("alert"), TargetID: &id, TS: time.Now().UTC(), }) if r.Header.Get("HX-Request") == "true" { w.Header().Set("HX-Redirect", "/alerts?"+r.URL.RawQuery) w.WriteHeader(stdhttp.StatusNoContent) return } stdhttp.Redirect(w, r, "/alerts", stdhttp.StatusSeeOther) } // handleUIAlertResolve is POST /alerts/{id}/resolve. func (s *Server) handleUIAlertResolve(w stdhttp.ResponseWriter, r *stdhttp.Request) { u := s.requireUIUser(w, r) if u == nil { return } id := chi.URLParam(r, "id") if id == "" { stdhttp.Error(w, "missing id", stdhttp.StatusBadRequest) return } if err := s.deps.Store.Resolve(r.Context(), id, time.Now().UTC()); err != nil { slog.Warn("ui alerts: resolve", "err", err) } _ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{ ID: ulid.Make().String(), UserID: &u.ID, Actor: "user", Action: "alert.resolve", TargetKind: ptr("alert"), TargetID: &id, TS: time.Now().UTC(), }) if r.Header.Get("HX-Request") == "true" { w.Header().Set("HX-Redirect", "/alerts?"+r.URL.RawQuery) w.WriteHeader(stdhttp.StatusNoContent) return } stdhttp.Redirect(w, r, "/alerts", stdhttp.StatusSeeOther) }