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.
190 lines
6.7 KiB
Markdown
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).
|