Release 2026.08.02-1

SQLite replaces Postgres, and two fixes from using the thing.

- The database is a file under ./storage instead of a second container. Ten
  members never needed a database server, and the driver is pure Go, so the
  build stays CGO_ENABLED=0 and the dependency count is unchanged. One bind
  mount is now the whole backup: no pgdata, no healthcheck-gated depends_on,
  no startup retry loop. Timestamps are UTC text, idle_ttl is seconds, the
  divisive/unified boards carry their own stddev, and foreign keys are on by
  pragma. Tests get a database file each and run without any setup
- Invites are copied, not clicked. An invite is something to send, and the
  anchor opened the join form in the admin's own browser
- Feedback asks for more than faults: the footer reads "Ongelmia? Ideoita?
  Palautetta?" and the page behind it invites ideas rather than only bugs
- Kuuntele YouTubessa opens in a new tab, so a half-typed review survives it
This commit is contained in:
Esa Kataja
2026-08-02 20:58:52 +03:00
parent 400b5d3833
commit 0f15ae0bfc
33 changed files with 480 additions and 396 deletions
+10 -9
View File
@@ -47,7 +47,7 @@ func (a *app) reportPage(w http.ResponseWriter, r *http.Request) {
// Seeing your own past reports is what stops the same bug arriving four times.
func (a *app) myReports(ctx context.Context, userID int64) ([]*report, error) {
rows, err := a.pool.Query(ctx, `
rows, err := a.db.QueryContext(ctx, `
select id, body, coalesce(page, ''), resolved_at, created_at
from reports where user_id = $1 order by created_at desc`, userID)
if err != nil {
@@ -79,7 +79,7 @@ func (a *app) createReport(w http.ResponseWriter, r *http.Request) {
}
// "Only on my phone" is the most common bug report and this answers it without asking.
_, err := a.pool.Exec(r.Context(), `
_, err := a.db.ExecContext(r.Context(), `
insert into reports (user_id, body, page, user_agent) values ($1, $2, nullif($3, ''), $4)`,
me.ID, body, from, clean(r.Header.Get("User-Agent"), 300))
if err != nil {
@@ -95,7 +95,7 @@ func (a *app) createReport(w http.ResponseWriter, r *http.Request) {
// --- admin ---
func (a *app) adminReports(w http.ResponseWriter, r *http.Request) {
rows, err := a.pool.Query(r.Context(), `
rows, err := a.db.QueryContext(r.Context(), `
select rep.id, rep.body, coalesce(rep.page, ''), coalesce(rep.user_agent, ''),
u.name, rep.resolved_at, rep.created_at
from reports rep join users u on u.id = rep.user_id
@@ -130,8 +130,9 @@ func (a *app) resolveReport(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
if _, err := a.pool.Exec(r.Context(),
`update reports set resolved_at = case when resolved_at is null then now() end where id = $1`,
if _, err := a.db.ExecContext(r.Context(),
`update reports set resolved_at = case when resolved_at is null then datetime('now') end
where id = $1`,
id); err != nil {
adminError(w, "reports", err)
return
@@ -147,12 +148,12 @@ func (a *app) adminDeleteSong(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
tag, err := a.pool.Exec(r.Context(), `delete from songs where id = $1`, id)
res, err := a.db.ExecContext(r.Context(), `delete from songs where id = $1`, id)
if err != nil {
adminError(w, "songs", err)
return
}
if tag.RowsAffected() > 0 {
if affected(res) > 0 {
removeFile(a.audioPath(id))
slog.Info("song deleted by admin", "ctx", "songs", "song", id)
a.flash(w, "Kappale poistettu.")
@@ -170,9 +171,9 @@ type adminSong struct {
}
func (a *app) adminSongs(ctx context.Context) ([]adminSong, error) {
rows, err := a.pool.Query(ctx, `
rows, err := a.db.QueryContext(ctx, `
select s.id, s.title, s.artist, u.name,
(select count(*) from reviews r where r.song_id = s.id)::int, s.created_at
(select count(*) from reviews r where r.song_id = s.id), s.created_at
from songs s join users u on u.id = s.submitted_by
order by s.created_at desc`)
if err != nil {