Files
foodster/README.md
T
Esa Kataja d15f741ab1 Add dish CRUD to the UI, including inline from Kirjaa
The catalog could only be filled by importing JSON, which is a poor way to
add the one dish you are about to eat.

Ruoat now covers PRD §7.3 in full: add and edit mains with their categories
and has_sides, add and edit sides, and delete either. Deletes are soft, so a
log entry keeps resolving the dish it used and the freed name can be reused.
Validation messages are Finnish and the rejected form comes back filled in
rather than blank. Bulk import moves into a details element, since it is now
the occasional path rather than the only one.

Kirjaa gets the same ability without the detour: a search that finds nothing
offers to add what was typed, and saving creates the dish and continues
straight to the sides step. An empty catalog shows the same card instead of
dead-ending on a link to another tab, and the search box is no longer hidden
behind the empty state.

The importer's own insert is gone; it and the UI both go through createMain
and createSide, so duplicate detection lives in one place and reason() can
match on errNameTaken instead of poking at driver strings.
2026-09-05 18:44:09 +03:00

171 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** — 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:
- Light / dark theme switch, saved per device (PRD §7.4).
- A proper app header and a `favicon.svg`.
- 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).