Files
foodster/README.md
T
Esa Kataja e4503e8433 Build the Kirjaa log flow and Historia list
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.
2026-09-05 18:29:38 +03:00

170 lines
5.9 KiB
Markdown

# 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** — import a bundle of dishes by paste or file upload.
Still to build:
- Adding, editing and deleting dishes **in the UI** (PRD §7.3). Right now the
catalog can only be filled by importing a bundle.
- Light / dark theme switch, saved per device (PRD §7.4).
- 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
```
## 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).