Add stats, profiles, avatars and palaute

Step 6. The surfaces around the review loop.

- Nine leaderboards, ordered and limited in SQL, each with a deterministic
  tie-break so a tied board doesn't reshuffle between reloads. Min 3 reviews
  to qualify, for reviewer boards too
- Profiles show counts and history-wide averages and the member's songs,
  never a list of their reviews — per-song opinion stays gated
- Avatars: 5MB in, 256px JPEG out, ffmpeg's re-encode being the validation.
  No upload still means initials, and avatars are public
- Changing your own password requires the current one and drops your other
  sessions
- Palaute: free text plus the page you were on, carried in a footer link, and
  the user agent from the header. Reporters see their own; the admin resolves
  them with a timestamp rather than a status enum
- Admin gained the song list with delete, the reports page, and an open-report
  count on the dashboard

Two theme fixes the screenshots caught: leaderboard ranks need a CSS counter
because display:grid suppresses list markers, and count-based boards were
printing 3.0 where they mean 3.
This commit is contained in:
Esa Kataja
2026-07-31 22:34:04 +03:00
parent f33f4fa4d6
commit f1e907bac3
16 changed files with 991 additions and 6 deletions
+8 -1
View File
@@ -269,12 +269,19 @@ func (a *app) deleteSong(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("/songs/%d", id), http.StatusSeeOther)
return
}
os.Remove(a.audioPath(id))
removeFile(a.audioPath(id))
slog.Info("song deleted", "ctx", "songs", "song", id)
a.flash(w, "Kappale poistettu.")
http.Redirect(w, r, "/songs", http.StatusSeeOther)
}
// A missing file is fine — the row is gone either way — but anything else is worth knowing about.
func removeFile(path string) {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
slog.Error("remove file", "ctx", "songs", "error", err, "path", path)
}
}
// --- audio ---
// Auth-gated, Range-capable, and not under /api because it serves bytes rather than JSON.