# Foodster A self-hosted dinner log and meal suggester for a single household. Record what the family actually ate, and — later — let the app propose seven dinners drawn from that history. The interface is in Finnish. The code, comments and documentation are in English. See [PRD.md](PRD.md) for the full specification. ## Status **Stage 1 — eating history: in development.** The meal catalog and the daily log come first, because the suggester is worthless until there are a few weeks of real history to weight against. Working: - **Kirjaa** — log a dinner: pick a dish, tick sides, save. Dishes are sized by how often they are eaten. Edit or delete the day's entry. - **Historia** — every day back to the first entry, with unlogged days shown as explicit gaps. - **Ruoat** — add, edit and delete mains and sides, or import a whole bundle by paste or file upload. Deletes are soft, so old log entries keep showing the dish they used. Still to build: - **UI polish.** Checkbox chips hide the native control and signal state only through background colour, so "Tarjoillaan lisukkeiden kanssa" gives no clear read of on versus off. Show a real checkbox. Same applies to the category and side chips. - Light / dark theme switch, saved per device (PRD §7.4). - A proper app header. - Stage 2: the seven-meal suggester, which starts once there is history to weight against. ## Stack One static Go binary. No Node.js, no bundler, no separate database server. | | | |---|---| | Language | Go 1.27 | | HTTP | stdlib `net/http` + `http.ServeMux` | | Views | [templ](https://templ.guide), server-rendered | | Interactivity | [Datastar](https://data-star.dev) — signals and DOM patching in one ~11 kB script | | Styling | hand-written CSS, `light-dark()` for themes | | Database | SQLite via `modernc.org/sqlite` (pure Go) | | Auth | HTTP Basic, one shared household password | | Runtime image | `FROM scratch` | ## Quick start ```sh cp .env.example .env # then edit it make run # http://localhost:8080 ``` `make` on its own lists every target: ``` make fix gofmt, templ fmt, go mod tidy make lint go vet, gofmt check, golangci-lint when installed make test go test ./... make build ./foodster make seed import a dish bundle (SEED=seeds/testi.json) make vendor re-download the Datastar client make image build and tag vYYYYMMDD-N (creates a git tag) make push push the newest tag and :latest make release image + push make up/down/logs compose ``` ## Importing dishes The **Ruoat** tab takes a bundle of mains and sides: paste the JSON or upload a file, and the app reports row by row what it did. ```json { "mains": [{"name": "Kanacurry", "categories": ["chicken"], "has_sides": true}], "sides": [{"name": "Riisi"}] } ``` Categories are `meat`, `chicken`, `fish` and `vegetarian` — stored in English even though the UI is Finnish. A dish may list several, which is how build-your-own meals like tortillas cover every category at once. `has_sides` defaults to `true` when omitted. Import is best-effort, never atomic. Valid rows land; invalid ones are skipped and named (`tuntematon kategoria`, `ei kategorioita`, `jo listalla`). Names are normalized to sentence case on the way in — whitespace collapses and the first letter is capitalized, the rest is left as typed, because Finnish capitalizes only the first word of a phrase. `keitetyt perunat` is stored as `Keitetyt perunat`, while `BBQ-kylkeä` and `Kotipizza` keep their capitals. Duplicates are caught case-insensitively, so re-importing a bundle is safe. The same importer runs from the command line when you just want to repopulate a scratch database: ```sh make seed # or: SEED=seeds/other.json make seed ``` ## Icons `cmd/foodster/static/favicon.svg` is the browser-tab icon and follows the system theme. The home-screen icons are PNGs, because a home-screen icon cannot be transparent and must not change with the theme; they are rasterised from `assets/icon.svg`, which is opaque and keeps the artwork inside the central 80% so Android can mask it to any shape. ```sh make icons # rsvg-convert, then optipng -o7 ``` The PNGs are committed so the build needs no rasterizer. Re-run `make icons` after editing `assets/icon.svg`. ## Migrations Numbered SQL files in `cmd/foodster/migrations`, embedded in the binary and applied in filename order at startup. Each runs in a transaction and is recorded in `schema_migrations`, so it applies exactly once. Adding one means dropping a new `NNNN_what_it_does.sql` into that directory. Never edit a migration that has already shipped — a released file has run on a live database and will not run again. ## Configuration Everything is environment variables. `.env` is gitignored; start from `.env.example`. | Variable | Default | Purpose | |---|---|---| | `FOODSTER_PASSWORD` | *required* | Shared password. The app will not start without it. | | `FOODSTER_DB` | `./data/foodster.db` | SQLite file path; the directory is created if missing. | | `FOODSTER_UID` / `FOODSTER_GID` | `1000` | Host owner of `./data`, for the bind mount. | | `TZ` | `Europe/Helsinki` | Used for every calendar-day calculation. | | `FOODSTER_REPO` | *required to build* | Image repository, no tag. | | `FOODSTER_TAG` | `latest` | Tag to run under compose. | | `FOODSTER_PORT` | `8080` | Host port to publish. | Set `TZ` in development too. Under UTC the date rolls over three hours late, which is exactly when dinner gets logged. ## Deployment Images are built with Podman and run under Docker Compose on a LAN server. They are OCI images, so either engine works. ```sh make release # build, tag, push # on the server: docker compose pull && docker compose up -d ``` Versions are CalVer — `vYYYYMMDD-N`, where `N` is the Nth build that day. The running version is served at `GET /healthz`, which is the one route outside authentication. There is no database container. SQLite lives in `./data`, bind-mounted into the container, so a backup is `cp -r data` and you can inspect the file with any sqlite client without going through the engine. That directory must exist and be owned by the user compose runs as — `make up` creates it, and `FOODSTER_UID`/`FOODSTER_GID` in `.env` tell the container who that is. Get them from `id -u` and `id -g`. ## Security Access is a single shared password over HTTP Basic — no accounts, no sessions. Credentials are compared in constant time, but Basic auth sends them in cleartext, so this belongs on a private LAN. Put TLS in front of it before exposing it anywhere else. ## Mockups `mockups/` holds standalone HTML design studies. Open them directly in a browser; they are references, not part of the build. ## License [MIT](LICENSE).