Files
foodster/README.md
T
Esa Kataja 0538d61ba6 Add a favicon, home-screen icons and a web manifest
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.
2026-09-05 18:58:42 +03:00

190 lines
6.7 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:
- **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).