Merging a pull request into main is now the whole release. A Gitea
Actions workflow derives the CalVer tag, builds the image and pushes
it with :latest, so nothing is built locally any more.
That made image/push/release redundant, and with them the .release-tag
state file and the main-branch guard — the workflow only runs on main,
which is protected, so the guard had nothing left to catch. The digest
verification went too: it guarded a `make -j` race between image and
push that cannot happen in a single CI job.
seed, icons and vendor ran a few times a year and are written out in
the README instead. Makefile: 151 lines to 71.
Docs referenced the removed targets in sixteen places, including a
CONTRIBUTING note claiming the branch check "has to be local".
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
Tähteet — leftovers. Not a dish: it exists so a day can be recorded as "we ate what was already there" without inventing a meal nobody cooked. A special-flagged row created by migration 0002; never in the catalog, never editable, and excluded from the stage-2 suggester outright. It gets its own mark, a grey lidded tub — which exposed a bug where no categories drew the same icon as several.
The real dish list. The seed bundle is now your December 2024 list: 37 mains, 9 sides. Letut and Pannari dropped as not-dinners, Kanakintut folded into Broilerin koipireidet, has_sides assigned by rule.
The day list stays put. Choosing a day used to swap a panel in above the list and drop that day out of it, so rows below jumped up under the tap. The list is now the page; the selected day expands where it sits, with anchors so the viewport lands on the day rather than the top.
Release pipeline fixed. The last release silently pushed the previous image: release declared image and push as prerequisites and your make runs -j16, so they raced. Also, push re-derived the tag by date-sorting, which is ambiguous when two tags share a commit. It now reads what image recorded, and verifies afterwards that the registry serves what was built.
Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #2
Structure
- Kirjaa and Historia are one page. They were two views of the same thing — every history row already linked into the logger, and the logger had a day switcher. Two tabs instead of three. Also closed a gap: on an already-logged day there was no way to swap to a different dish, only to re-pick its sides.
- Ruoat → Ruuat, label and route.
- The catalog has a structure. It had no top-level headings at all — the mains simply began with "Liha". Both halves now carry a heading and a count, categories are visibly subordinate, and the add/edit forms collapse instead of filling the screen before any content.
Finding things
- Dishes grouped by category on both screens, Sekalaiset for multi-category ones. Derived from the stored set, not a fifth category, so one Tortillat still covers all four for the §8.1 suggester later.
- Live search on both lists, 250 ms after typing stops. Both remain plain GET forms, so they still filter with JavaScript off.
- History is paged 30 days at a time — it previously rendered every day back to the first entry, forever.
Correctness
- Future meals refused. The picker offered them and ?pvm= accepted them.
- today() wasn't midnight, so it never equalled a date parsed from ?pvm= — after saving, the card read "la 5.9. kirjattu" instead of "Tänään kirjattu".
- Deletes ask first, for dishes and logged meals. The meal is the more destructive: a dish is only soft-deleted.
- DB open failures name the path and uid, instead of unable to open database file (14).
Visual
- Category icons replace colour dots — steak, drumstick, fish, leaf, quartered circle.
- Row actions are a pencil and a bin; the header has a surface.
Housekeeping
- Datastar SDK dropped — one JSON decode was pulling in four modules including an HTTP compression stack. Five lines replace it.
- Release policy documented: main protected, releases arrive as PRs.
Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #1
The favicon is a bowl of soup: this household's catalog is half keitto, so it
is at least honest. favicon.svg follows the system theme in the tab strip.
Home-screen icons cannot do the same. They must be opaque and must not change
with the theme, so assets/icon.svg is a separate fixed-colour source, with the
artwork inside the central 80% for Android to mask to any launcher shape. The
same 512 is declared maskable in the manifest.
`make icons` rasterises with rsvg-convert and compresses with
`oxipng -o max --zopfli`, which beat `optipng -o7` at every size — 5860 bytes
against 6109 for the three files. Plain `oxipng -o max` was not an upgrade: it
won the two small icons and lost the 512, ending up larger overall. All output
verified pixel-identical to the rasterizer. The PNGs are committed so the
build needs no rasterizer.
Two things that only fail on a real device, so both are covered by smoke
checks: Go has no mime entry for .webmanifest and served it as octet-stream,
which browsers ignore; and a manifest fetch carries no credentials by default,
so behind Basic auth it needs crossorigin="use-credentials" or it 401s.
Stage 1's point is collecting eating history, so the logging path is the one
that has to be frictionless: pick a dish, tick sides, done.
Kirjaa:
- dishes ordered and sized by how often they have been eaten, so the likely
answer is the biggest target on the screen
- picking a dish opens the sides step; a dish with has_sides false says so
instead of offering an empty list
- saving redirects, so a refresh cannot double-post
- a day already logged shows the entry with edit and delete. Editing reopens
the sides step with the existing sides ticked, which makes editing and
creating the same screen
- day switcher and a server-side search over the catalog
Historia walks back day by day to the oldest entry, so a day nobody wrote
down appears as an explicit gap rather than quietly missing.
No JavaScript is involved: every interaction is a link or a form, and the
checked-chip styling is :has(input:checked). Datastar stays loaded but unused
until an interaction genuinely needs to avoid a page load.
Also fix a Makefile ordering bug: lint did not depend on generate, so `go vet`
could run against templ output that was being rewritten. check now runs its
phases as sub-makes so `make -j` cannot interleave them.
Records two features that are specified but not built: dish CRUD in the UI
(§7.3, only import exists today) and a per-device light/dark switch (§7.4).
The switch must show the state that is active, not the one clicking produces.
SQLite writes three files — the database plus its -wal and -shm companions.
Putting them in one directory means a deployment mounts a single path and a
backup copies a single directory.
- FOODSTER_DB defaults to ./data/foodster.db, and openDB creates the parent
directory on startup rather than failing on a fresh checkout
- compose bind-mounts ./data instead of using a named volume, so the file can
be listed, copied and opened with any sqlite client without going through
the container engine
- the image runs as UID 65534, which cannot write to a host directory owned
by someone else, so compose now sets user: from FOODSTER_UID/FOODSTER_GID
- `make up` creates ./data first: left to the engine it appears root-owned
and the app silently cannot write to it
Bring up the stage 1 skeleton described in the PRD, enough that the app
builds, serves, and can be populated with dishes.
- net/http server with shared-password Basic auth, /healthz outside it,
graceful shutdown, and TZ-aware calendar days
- SQLite via modernc (pure Go, static binary), opened with WAL and a single
connection
- migration runner: numbered SQL files embedded and applied once each inside
a transaction, recorded in schema_migrations
- bundle import (PRD §7.3) as a live feature on the Ruoat tab: paste JSON or
upload a file, get a per-row Finnish report. The same importer is reachable
as `foodster -import` for repopulating a scratch database
- templ views and hand-written CSS with light-dark() theming; the Datastar
v1.0.3 client is vendored, since the Go SDK ships no browser asset and a
CDN would break an offline LAN
Names are normalized to sentence case rather than title case: Finnish
capitalizes only the first word of a phrase, so "Keitetyt perunat" is right
and "Keitetyt Perunat" is not. PRD §6 and §7.3 are amended to match.
Testing is behind make targets rather than ad-hoc commands: `make check` runs
lint, unit tests and scripts/smoke.sh, which exercises auth, static assets
and every import path against a scratch database on a spare port.
Foodster is a self-hosted dinner log and meal suggester for one household.
Stage 1 (eating history) is in development; stage 2 (the suggester) follows
once there is enough history to weight against.
Replace the PRD's original stack (Nuxt, Postgres, Drizzle, Pico CSS) with
Go 1.27, net/http, templ, Datastar and SQLite — one static binary, no
Node.js in the build. Sections 3, 4, 5, 9, 10 and 11 are rewritten to match.
Record the decisions made while reviewing mockups:
- the UI is written in Finnish; PRD, code and comments stay English
- access is one shared password over HTTP Basic rather than open on the LAN
- images are CalVer vYYYYMMDD-N, built with Podman, run under Docker Compose
- registry coordinates live in .env, so no infrastructure detail is
committed and the MIT publication option stays open
mockups/ holds standalone HTML design studies. log-fi.html is the current
one; the others are superseded exploration kept for reference.