A submission failed with HTTP 403 against a freshly published image. The
cause was not Alpine, which carries the current yt-dlp: it was the apk layer
being reused from an older build, so the image shipped a yt-dlp two months
behind while /healthz reported today's tag.
"A rebuild is the update" only holds if the layer is genuinely re-run, so
the image target now passes --pull --no-cache. Slower, and the only thing
that makes the release honest about what is inside it.
A download that died on an HTTP 403 told the submitter "HTTP Error 403:
Forbidden" and told the log "error: exit status 1". The tool's stderr went
into status_msg and nowhere else, so the one person who could act on it saw
nothing. Exactly backwards.
Failures now go through a.fail: the submitter gets a sentence and an eight
character code, the log gets that code, the stage, the submission, the
source URL and the stderr tail. Quote the code, grep the log, find the line.
Every record carries file:line now, and LOG_LEVEL sets the threshold —
failures are logged at error, so no level hides them.
The admin was a set of env credentials on its own loopback listener. That
bought network isolation, and charged a second port to tunnel and proxy and
a second credential in the password manager. It also sat outside the
SameSite protection the member cookie already had, and left every ban and
password reset with no actor to log.
is_admin on users reuses what was already there: the session, the login
rate limiter, ban-drops-sessions, CSRF. /admin is now a route on the member
mux. A member without the flag gets 404 rather than 403 — the pages are
none of their business, and "forbidden" confirms there is something to be
forbidden from.
Registration needs an invite and invites come from /admin, so an empty
database cannot grow its first user. seedAdmin breaks that circle exactly
once, from ADMIN_EMAIL and ADMIN_PASSWORD, and does nothing against a
database that already has users.
An admin cannot ban themselves: banning drops the target's sessions, and
nothing would be left that could undo it.
This reverses decision 8, which is rewritten rather than deleted, along
with the admin entry in the CONTEXT.md vocabulary.
docs/deployment.md is the server-side procedures: the compose file a server
runs, building and publishing a release, first deployment, reverse proxy,
upgrades and rollback, backups and restore, and a troubleshooting table.
The compose file lives in the manual rather than in the repository, because
the one at the root builds from source and is what development wants. The
server's pulls a published image, pins a release tag, and publishes the
admin port on the host's loopback instead of every interface — the panel is
Basic Auth and nothing else, so where that port is bound is the whole of its
security.
.dockerignore keeps the image build off storage/ (the live database and the
audio), .env (the admin password) and the leftover pgdata, which the build
cannot read anyway and which fails it outright.