Scaffold the Go app: auth, migrations, bundle import
Bring up the stage 1 skeleton described in the PRD, enough that the app builds, serves, and can be populated with dishes. - net/http server with shared-password Basic auth, /healthz outside it, graceful shutdown, and TZ-aware calendar days - SQLite via modernc (pure Go, static binary), opened with WAL and a single connection - migration runner: numbered SQL files embedded and applied once each inside a transaction, recorded in schema_migrations - bundle import (PRD §7.3) as a live feature on the Ruoat tab: paste JSON or upload a file, get a per-row Finnish report. The same importer is reachable as `foodster -import` for repopulating a scratch database - templ views and hand-written CSS with light-dark() theming; the Datastar v1.0.3 client is vendored, since the Go SDK ships no browser asset and a CDN would break an offline LAN Names are normalized to sentence case rather than title case: Finnish capitalizes only the first word of a phrase, so "Keitetyt perunat" is right and "Keitetyt Perunat" is not. PRD §6 and §7.3 are amended to match. Testing is behind make targets rather than ad-hoc commands: `make check` runs lint, unit tests and scripts/smoke.sh, which exercises auth, static assets and every import path against a scratch database on a spare port.
This commit is contained in:
@@ -84,7 +84,11 @@ in English.
|
||||
|
||||
### Main dish (stage 1)
|
||||
- `id`
|
||||
- `name` — stored in Title Case (server normalizes on save).
|
||||
- `name` — stored in sentence case: the server collapses whitespace and
|
||||
capitalizes the first letter only, leaving the rest as typed. Finnish
|
||||
capitalizes just the first word of a phrase, so "Keitetyt perunat" is
|
||||
correct and "Keitetyt Perunat" is not. Existing capitals are preserved, so
|
||||
"BBQ-kylkeä" and "Kotipizza" survive.
|
||||
- `categories` — non-empty set drawn from `meat`, `chicken`, `fish`,
|
||||
`vegetarian`. Most mains will have a single category. Dishes where each
|
||||
diner builds their own plate from a shared spread (e.g., tortillas,
|
||||
@@ -101,7 +105,7 @@ in English.
|
||||
|
||||
### Side dish (stage 1)
|
||||
- `id`
|
||||
- `name` — stored in Title Case.
|
||||
- `name` — stored in sentence case, as for mains.
|
||||
- `created_at`
|
||||
- `deleted_at` — nullable. Same soft-delete semantics as mains.
|
||||
|
||||
@@ -168,8 +172,8 @@ regenerates or swaps.
|
||||
### 7.3 Admin (stage 1 — catalog; stage 2 — settings)
|
||||
|
||||
- Add / edit / delete main dishes (with categories and `has_sides`). Names
|
||||
are normalized to Title Case on save. Delete is a soft-delete.
|
||||
- Add / edit / delete side dishes. Same Title Case normalization and
|
||||
are normalized to sentence case on save. Delete is a soft-delete.
|
||||
- Add / edit / delete side dishes. Same sentence-case normalization and
|
||||
soft-delete behavior.
|
||||
- Duplicate detection on create/edit is **case-insensitive** against the
|
||||
set of non-soft-deleted items ("chicken curry" and "Chicken Curry" are
|
||||
@@ -191,8 +195,8 @@ regenerates or swaps.
|
||||
```
|
||||
- Import is **best-effort, not atomic**: valid rows are inserted, invalid
|
||||
rows (bad categories, missing fields, duplicates) are skipped and
|
||||
reported back to the user in a per-row summary. Names are Title-Cased
|
||||
on import; duplicate detection is case-insensitive.
|
||||
reported back to the user in a per-row summary. Names are normalized to
|
||||
sentence case on import; duplicate detection is case-insensitive.
|
||||
- Settings page for the cooldown window (default **14 days**, configurable).
|
||||
*(Stage 2 — only relevant once the suggester exists.)*
|
||||
|
||||
@@ -290,9 +294,20 @@ build and no asset bundler.
|
||||
volume; there is no separate database service. A single household writing
|
||||
one row per day does not need Postgres.
|
||||
- **Data access**: `database/sql` and hand-written SQL. No ORM.
|
||||
- **Schema**: a single `schema.sql` embedded with `embed.FS` and applied on
|
||||
startup with `CREATE TABLE IF NOT EXISTS`. A migration tool arrives the
|
||||
first time a live table genuinely needs altering, not before.
|
||||
- **Migrations**: numbered `.sql` files under `cmd/foodster/migrations`,
|
||||
embedded with `embed.FS` and applied in filename order on startup. Each one
|
||||
runs inside a transaction and is recorded in a `schema_migrations` table, so
|
||||
it applies exactly once. No migration library — the runner is about sixty
|
||||
lines of `database/sql`. There are no down-migrations: restoring the
|
||||
database file is the rollback for a single-household app.
|
||||
- **Bundle import**: the §7.3 mass import is a live feature of the running
|
||||
app, on the Ruoat tab — paste JSON or upload a file, get a per-row report
|
||||
back. A plain multipart form rather than a Datastar round trip, since the
|
||||
response is a whole-page report and a form needs no client code. Uploads
|
||||
are capped at 1 MiB. The same importer is also reachable as
|
||||
`foodster -import <file.json>` for repopulating a scratch database without
|
||||
starting the server; `seeds/testi.json` is the committed fixture. One code
|
||||
path serves both, so the format is exercised twice.
|
||||
- **Time**: the `TZ` environment variable, defaulting to `Europe/Helsinki`,
|
||||
loaded via `time.LoadLocation` and fatal on a bad value — a silent fallback
|
||||
to UTC would shift logged dinners to the wrong calendar day. `time/tzdata`
|
||||
@@ -342,7 +357,11 @@ on the server and run with Docker Compose.
|
||||
has no shell to run one and `restart: unless-stopped` already covers a dead
|
||||
process. Adding one would mean giving the binary a `-healthcheck` flag that
|
||||
calls its own endpoint.
|
||||
- Schema is applied on app start.
|
||||
- Pending migrations are applied on app start.
|
||||
- The Datastar client is vendored at `cmd/foodster/static/datastar.js` and
|
||||
served from the app's own origin — the SDK ships no browser asset, and a
|
||||
CDN link would break an offline LAN. `make vendor` refreshes it; the pinned
|
||||
version lives in the `Makefile` and in the file's first line.
|
||||
- No internet exposure; the server binds to the LAN.
|
||||
|
||||
## 11. Licensing
|
||||
|
||||
Reference in New Issue
Block a user