package handler import ( "errors" "html/template" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/prabhupadasays/spsays-bff/internal/repository" "github.com/prabhupadasays/spsays-bff/internal/service" ) // StatsHandler renders a single-page HTML snapshot of the system — // aggregate counts plus the most-recent feedback rows. Mounted at /stats // (outside /api/v1, since it returns HTML not JSON) and intentionally // public per current product call. If that ever needs to change, wrap // the route in middleware in server.go. // // It also handles the "Mark resolved" form POST per feedback row, which // stamps resolved_at (one-way) and fires the author thank-you email via // the feedback service. type StatsHandler struct { stats *repository.StatsRepo feedback *repository.FeedbackRepo svc *service.FeedbackService } func NewStatsHandler(stats *repository.StatsRepo, feedback *repository.FeedbackRepo, svc *service.FeedbackService) *StatsHandler { return &StatsHandler{stats: stats, feedback: feedback, svc: svc} } func (h *StatsHandler) Render(w http.ResponseWriter, r *http.Request) { snap, err := h.stats.Snapshot(r.Context()) if err != nil { http.Error(w, "stats query failed: "+err.Error(), http.StatusInternalServerError) return } recent, err := h.feedback.Recent(r.Context(), 25) if err != nil { http.Error(w, "recent feedback query failed: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") // Caching off — this is an ops snapshot; staleness defeats the point. w.Header().Set("Cache-Control", "no-store") if err := statsTpl.Execute(w, map[string]any{ "Stats": snap, "Feedback": recent, "GeneratedAt": time.Now().Format("2006-01-02 15:04:05 MST"), }); err != nil { // Headers already flushed — best we can do is bail mid-stream. return } } // AcknowledgeFeedback handles the per-row "Acknowledge" form POST. Marks // the row acknowledged (one-way) and emails the author that we've seen // it and started work, then 303-redirects back to /stats. Already- // acknowledged or unknown ids redirect silently. func (h *StatsHandler) AcknowledgeFeedback(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.svc.Acknowledge(r.Context(), id); err != nil && !errors.Is(err, service.ErrAlreadyAcknowledged) { http.Error(w, "acknowledge failed: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/stats", http.StatusSeeOther) } // ResolveFeedback handles the per-row "Mark resolved" form POST. It marks // the row resolved (one-way) and fires the author thank-you email, then // 303-redirects back to /stats so a refresh doesn't re-submit. Already- // resolved or unknown ids redirect silently — the desired end state is // already true. func (h *StatsHandler) ResolveFeedback(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.svc.Resolve(r.Context(), id); err != nil && !errors.Is(err, service.ErrAlreadyResolved) { http.Error(w, "resolve failed: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/stats", http.StatusSeeOther) } var statsTpl = template.Must(template.New("stats").Funcs(template.FuncMap{ "fmtTime": func(t time.Time) string { return t.Format("2006-01-02 15:04") }, }).Parse(statsHTML)) const statsHTML = `
No feedback yet.
{{end}}No feedback yet.
{{end}} `