release: repair the catalog 404 and stop the page jumping (#3)

Showstopper. Adding, editing or deleting a dish redirected to /ruoat, which stopped existing when the tab was renamed to Ruuat — every one of those actions ended on a 404. Live in v20260905-4. The tests missed it because they asserted only a 303; a redirect to a dead URL is still a 303. They now assert the target.

The page no longer jumps. Deleting a dish partway down the catalog, or opening a day in Kirjaa, sent the browser to the top. Both now patch in place via Datastar — bin, pencil, day rows, dish pills, save, delete, cancel and Näytä lisää. Links stay links and forms stay forms, so it works without JavaScript.

Non-production tabs are labelled. ENV=dev gives dev · Foodster.

Contributing guide added, and commits now take Conventional Commit types.

⚠️ Breaking: rewrite the server's .env in this deploy. Environment variables lost the FOODSTER_ prefix; the app refuses to start on an unset PASSWORD.

REPO=…  TAG=latest  PASSWORD=…  HOST=foodster.kessinen.com
ENV=prod  PUID=1000  PGID=1000  TZ=Europe/Helsinki

PUID/PGID rather than UID/GID — UID is read-only in bash and would be silently overwritten.

Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-09-05 21:03:04 +00:00
co-authored by Esa Kataja
parent 174652778b
commit d8810d288e
11 changed files with 533 additions and 136 deletions
+13 -9
View File
@@ -351,7 +351,7 @@ build and no asset bundler.
is imported because the runtime image carries no zoneinfo. All date logic
uses that location explicitly and never `time.Local`.
- **Auth**: HTTP Basic with one shared household password read from
`FOODSTER_PASSWORD`; the username is ignored. Compared using
`PASSWORD`; the username is ignored. Compared using
`subtle.ConstantTimeCompare` over SHA-256 digests so neither the value nor
its length leaks through timing. `/healthz` is the only route outside auth.
- **Exposure**: the app is served on a public hostname behind Traefik, which
@@ -395,21 +395,25 @@ on the server and run with Docker Compose.
`/data/foodster.db`, bind-mounted from `./data` on the host rather than
kept in a named volume, so the file can be listed and copied without going
through the container engine. Backup is `cp -r data`. Because the image
runs as UID 65534, compose sets `user:` from `FOODSTER_UID`/`FOODSTER_GID`
runs as UID 65534, compose sets `user:` from `PUID`/`PGID`
to match whoever owns that directory. `restart: unless-stopped`.
- **Configuration**, entirely through environment variables (see
`.env.example`):
- `FOODSTER_REPO` and `FOODSTER_TAG` — image coordinates.
- `FOODSTER_PASSWORD` — the shared password. Required; the app refuses to
start without it.
- `FOODSTER_DB` — database file path, default `./data/foodster.db`. The
directory is created on startup if missing.
- `FOODSTER_UID` / `FOODSTER_GID` — host owner of `./data`.
Names carry no application prefix: the container namespaces them already.
- `REPO` and `TAG` — image coordinates.
- `PASSWORD` — the shared password. Required; the app refuses to start
without it.
- `DB` — database file path, default `./data/foodster.db`. The directory is
created on startup if missing.
- `ENV` — anything but `prod` is prefixed to the browser tab title, so a
dev instance open beside the real one can be told apart.
- `PUID` / `PGID` — host owner of `./data`. Not `UID`, which is read-only
in bash and would be replaced by the invoking shell's own value.
- `TZ` — default `Europe/Helsinki`.
- The registry hostname exists only in `.env`, which is gitignored, because
§11 leaves open the possibility of publishing this repository.
- **Routing**: Traefik on an external `traefik` network, matching on
`FOODSTER_HOST` and terminating TLS. The container publishes no ports —
`HOST` and terminating TLS. The container publishes no ports —
doing so would put an unencrypted copy of the app on the host, bypassing
the proxy. The hostname lives in `.env` rather than `compose.yaml`, so no
infrastructure detail is committed.