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.
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.
The theme handoff encoded as CSS custom properties rather than a Tailwind
config, since there is no Tailwind here. Palette, spacing, radii, shadows and
motion follow it as written; docs/theme.md lists what differs and why.
- Oswald vendored as a 21KB variable woff2, latin subset, no CDN. Its phantom
weight 900 resolved to 700 — loading a weight you don't have is what made
the brand render differently per platform
- color-scheme: dark makes the native audio element fit the palette, which
was the handoff's complaint about it
- Songs are text cards rather than artwork tiles, because there is no
artwork. The unreviewed state keeps its red-brown border and gains a badge,
so it is never carried by colour alone
- Nav is the three-column grid; the mobile menu is <details>, no JS
- Flash messages became bottom-right toasts
Favicon carried over from the Nuxt project.
Step 2 of the build order. The admin mints an invite link, the recipient
registers with it, and from then on has a session.
- The invite is spent in the same transaction that creates the account, so a
failed signup leaves the code usable
- Sessions are idle timeouts, 24h or 30 days with remember me, read from a
cookie or a bearer header, extended at most once a minute
- Ban is a reversible toggle that drops the member's live sessions
- No password minimum; login is rate limited instead, 10 failures per email
in 15 minutes, cleared by a correct password
- Invite codes render as links carrying ?code=, which the register form
prefills; PUBLIC_URL makes them pasteable from the loopback admin panel
Tests cover invite spending, the idle timeout, ban, and the rate limiter.