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
+48 -44
View File
@@ -1,52 +1,56 @@
-- Timestamps are declared `timestamp` and hold UTC 'YYYY-MM-DD HH:MM:SS': the declared type is what
-- makes the driver hand them back as time.Time, and a fixed-width UTC string is what makes
-- `order by created_at` and `expires_at > datetime('now')` mean what they say.
create table users (
id bigserial primary key,
name text not null,
email text not null unique,
password_hash text not null,
id integer primary key autoincrement,
name text not null,
email text not null unique,
password_hash text not null,
avatar text,
banned boolean not null default false,
created_at timestamptz not null default now()
banned integer not null default 0,
created_at timestamp not null default (datetime('now'))
);
create table sessions (
token text primary key,
user_id bigint not null references users (id) on delete cascade,
idle_ttl interval not null,
expires_at timestamptz not null,
created_at timestamptz not null default now()
token text primary key,
user_id integer not null references users (id) on delete cascade,
idle_ttl integer not null, -- seconds; SQLite has no interval type
expires_at timestamp not null,
created_at timestamp not null default (datetime('now'))
);
create index on sessions (user_id);
create index sessions_user on sessions (user_id);
create table invites (
id bigserial primary key,
code text not null unique,
is_valid boolean not null default true,
created_at timestamptz not null default now()
id integer primary key autoincrement,
code text not null unique,
is_valid integer not null default 1,
created_at timestamp not null default (datetime('now'))
);
create table songs (
id bigserial primary key,
title text not null,
artist text not null,
genre text not null,
id integer primary key autoincrement,
title text not null,
artist text not null,
genre text not null,
description text,
-- LRC or plain text, told apart by whether the first line starts with '['. Not covered by the
-- lock: nobody reviewed the lyrics.
lyrics text,
audio_file text not null,
duration_seconds integer not null,
audio_file text not null,
duration_seconds integer not null,
source_url text,
submitted_by bigint not null references users (id),
created_at timestamptz not null default now()
submitted_by integer not null references users (id),
created_at timestamp not null default (datetime('now'))
);
create index on songs (created_at desc);
create index songs_created_at on songs (created_at desc);
create table submissions (
id bigserial primary key,
user_id bigint not null references users (id) on delete cascade,
status text not null default 'queued',
id integer primary key autoincrement,
user_id integer not null references users (id) on delete cascade,
status text not null default 'queued',
status_msg text,
source_url text,
tmp_path text,
@@ -55,37 +59,37 @@ create table submissions (
genre text,
description text,
lyrics text,
created_at timestamptz not null default now(),
created_at timestamp not null default (datetime('now')),
constraint submissions_status check (
status in ('queued', 'downloading', 'converting', 'ready', 'failed')
)
);
-- The submission quota (5 per rolling 24h, failures excluded) reads this.
create index on submissions (user_id, created_at desc);
create index submissions_user_created on submissions (user_id, created_at desc);
create table reviews (
id bigserial primary key,
song_id bigint not null references songs (id) on delete cascade,
reviewer_id bigint not null references users (id),
score integer not null check (score between 1 and 100),
text text not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
id integer primary key autoincrement,
song_id integer not null references songs (id) on delete cascade,
reviewer_id integer not null references users (id),
score integer not null check (score between 1 and 100),
text text not null,
created_at timestamp not null default (datetime('now')),
updated_at timestamp not null default (datetime('now')),
unique (song_id, reviewer_id)
);
create index on reviews (song_id);
create index reviews_song on reviews (song_id);
-- The queue asks "songs this member has not reviewed" — that lookup is by reviewer.
create index on reviews (reviewer_id, song_id);
create index reviews_reviewer_song on reviews (reviewer_id, song_id);
create table reports (
id bigserial primary key,
user_id bigint not null references users (id) on delete cascade,
body text not null,
id integer primary key autoincrement,
user_id integer not null references users (id) on delete cascade,
body text not null,
page text,
user_agent text,
resolved_at timestamptz,
created_at timestamptz not null default now()
resolved_at timestamp,
created_at timestamp not null default (datetime('now'))
);