package handler import ( "html/template" "net/http" "time" "github.com/prabhupadasays/spsays-bff/internal/repository" ) // 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 is READ-ONLY: feedback acknowledge/resolve actions live in the // authenticated admin panel, never on this public page. // // PRIVACY: because the page is public, it must never render personal contact // details. Anonymous submitters' email addresses are shown as "reply address // on file" only — and FeedbackRepo.Recent doesn't even SELECT the column, so // the address isn't in memory here to leak by accident. Anything added to this // template needs the same scrutiny; the admin panel is the place for PII. type StatsHandler struct { stats *repository.StatsRepo feedback *repository.FeedbackRepo } func NewStatsHandler(stats *repository.StatsRepo, feedback *repository.FeedbackRepo) *StatsHandler { return &StatsHandler{stats: stats, feedback: feedback} } 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 } } 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 = ` Stats · Prabhupāda Says

System snapshot

Generated {{.GeneratedAt}}

Users & activity

Users
{{.Stats.Users}}+{{.Stats.UsersLast7Days}} / 7d
Posts
{{.Stats.Posts}}+{{.Stats.PostsLast7Days}} / 7d
Collections
{{.Stats.Collections}}{{.Stats.PublicCollections}} public
Bookmarks
{{.Stats.Bookmarks}}
Reshares
{{.Stats.Reshares}}
Feedback
{{.Stats.Feedback}}+{{.Stats.FeedbackLast7Days}} / 7d

Feedback by category

{{if .Stats.FeedbackByCategory}} {{range .Stats.FeedbackByCategory}}
{{.Category}}{{.Count}}
{{end}} {{else}}

No feedback yet.

{{end}}

Recent feedback

{{if .Feedback}} {{range .Feedback}}
{{.Category}} {{fmtTime .CreatedAt}}
{{.Message}}
{{if .AuthorHandle}} @{{.AuthorHandle}}{{if .AuthorName}} · {{.AuthorName}}{{end}} {{else if .HasContactEmail}} anonymous · reply address on file {{else}} anonymous {{end}}
{{if .ResolvedAt}} {{if .AcknowledgedAt}}✓ Acknowledged {{fmtTime .AcknowledgedAt}}{{end}} ✓ Resolved {{fmtTime .ResolvedAt}} {{else if .AcknowledgedAt}} ✓ Acknowledged {{fmtTime .AcknowledgedAt}} {{else}} Pending {{end}}
{{end}} {{else}}

No feedback yet.

{{end}} `