Commit Graph
6 Commits
Author SHA1 Message Date
Esa Kataja b01d08b1e1 Move the package into src/
Thirty-seven entries in the root, most of them .go files. The assets had to
come along: //go:embed cannot reach outside its own directory, so
templates/, static/ and migrations/ live beside the code that embeds them,
and testdata/ beside the test that reads it. storage/ stays put — runtime
data, not source.

go build now needs -o. Without it the output would be named after the
package directory and collide with src/ itself.
2026-09-05 13:53:04 +03:00
Esa Kataja 10ec9d1d6e Move to Go 1.27, and fix initials for non-ASCII names
Initials() capped the loop with len(out) == 2, which counts bytes: a name
starting with Ä, Ö or Å filled the budget on its own and returned a single
letter. Count the initials taken instead.

go fix also wanted a strings.Builder here, but that allocates a string per
iteration to measure a two-character result. Took its SplitSeq suggestion
in lyrics.go, which drops an intermediate slice.
2026-09-05 12:46:08 +03:00
Esa Kataja 1fe5211ae6 Replace Postgres with SQLite
Ten members and a handful of songs a week never needed a database server,
and the server was the last thing making this a two-container deployment.
modernc.org/sqlite is pure Go, so CGO_ENABLED=0 survives and the dependency
count is unchanged: pgx out, sqlite in.

The port stayed small because the driver matches $1-style placeholders
against argument ordinals exactly as pgx does, so no query needed rewriting
for parameters. What did change:

- timestamptz becomes timestamp holding UTC 'YYYY-MM-DD HH:MM:SS'. The
  declared type is what makes the driver return time.Time, and the
  fixed-width UTC string is what makes ordering and comparison against
  datetime('now') mean what they say.
- interval has no equivalent: sessions.idle_ttl is seconds, and the review
  edit window travels as a SQLite date modifier string.
- No stddev_pop, so the divisive and unified boards spell the population
  formula out, guarded with max(0.0, ...) because cancellation returns a
  tiny negative when every score is identical.
- foreign_keys is off by default, so the cascades only exist because the
  pragma is set on every connection.

Drops the postgres service, its healthcheck, the depends_on gate, the
startup retry loop and POSTGRES_PASSWORD. ./storage is now the whole
backup. Tests get a fresh database file per test and run everywhere
instead of skipping without TEST_DATABASE_URL.
2026-08-02 20:47:41 +03:00
Esa Kataja ac2cfaebac Add CalVer versioning and rebuild the song page's fact line
Versions are YYYY.MM.DD-N, injected with -ldflags from a git tag, so no file
in the repo carries the number and a local build honestly says dev. The
version shows in the footer, the startup log and /healthz.

The song page's metadata was five different kinds of fact — artist, genre,
duration, provenance and a badge about the viewer — in one flat run with two
competing pills. Now the artist has its own line in the display face, and the
facts sit in four labelled cells like the spine of a cassette insert. The
submitter links to their profile, "oma kappale" became "lähetti: sinä", and
the clock time is gone: nobody needs the minute a song was published.
2026-07-31 23:38:54 +03:00
Esa Kataja 91e136055c Add the YouTube submission path
Step 5. A URL goes through the same pipeline as an upload — it just gains a
download step and a source_url.

- The host allowlist is checked on the parsed hostname before yt-dlp is
  invoked, so lookalikes and userinfo tricks are refused too
- yt-dlp -J reads metadata synchronously with a 15s timeout; a timeout leaves
  the fields blank rather than failing the submission
- Over-long tracks are refused from that metadata, before a byte is downloaded
- Failed URL submissions offer Yritä uudelleen with the typed text intact;
  uploads cannot retry, so they offer re-upload

Prefill takes track then title, and artist then creator then uploader, and
leaves a field blank rather than inventing one. testdata/ytdlp-noose.json is a
real dump of an ordinary upload, which has none of the music fields.

Also fixes a URL-only submit being blocked by the file input's required
attribute — HTML cannot express "one of these two", so the server says it.

The image now takes yt-dlp from Alpine 3.24 instead of pip, which drops
python3 and pip entirely; see decision 19.
2026-07-31 21:54:15 +03:00
Esa Kataja 80f82a82de Add skeleton: config, migrations, startup sweep, two listeners
Step 1 of the build order in docs/decisions.md. Boots, applies migrations
before serving, and serves a health check and an empty admin page.

- 001_init.sql is the full schema from docs/spec.md, including the check
  constraints and indexes the old app lacked
- The startup sweep fails submissions left mid-conversion by a restart; an
  in-process goroutine dies with the process and those rows would otherwise
  say converting forever
- Admin is Basic Auth from env on its own listener, fatal at startup when
  ADMIN_PASSWORD is unset
2026-07-31 19:41:01 +03:00