Commit Graph
10 Commits
Author SHA1 Message Date
Esa Kataja b8a46cdd30 Patch the day list in place instead of navigating
Kirjaa now works like the catalog: opening a day, picking a dish, saving,
deleting, cancelling and "Näytä lisää" all patch the list where it stands.
Nothing loads a page, so the scroll position never moves.

One builder serves all three paths. buildLog takes what the screen should
show — the day, an open dish, whether the entry is being changed or a delete
confirmed — and the page render, the patch and the post-write response all go
through it. After a save it is called with only the date, so the day comes
back closed rather than reopening the sides step it was just submitted from.

Links stay links and forms stay forms, with data-on:click__prevent and
data-on:submit__prevent layered over them, so it all still works with
JavaScript off. Each response patches a single element, so plain text/html is
enough here; the SSE writer is only needed by the catalog, where the list and
both forms have to move together.

The anchors added in the previous attempt are gone. They could never have
worked: the browser positions an anchor without knowing where the page was
scrolled, so it jumped regardless.
2026-09-05 23:45:53 +03:00
Esa Kataja e5e020457a Patch the catalog in place instead of navigating
Deleting a dish partway down the list sent the browser back to the top. The
first attempt at fixing it used anchors, which cannot work: the browser
positions the element with no knowledge of where the page was scrolled, so it
still jumps. Datastar was already loaded and doing nothing but search.

Every catalog action now patches. The bin, the pencil and Peruuta stay real
links; the forms stay real forms. Datastar intercepts them with
data-on:click__prevent and data-on:submit__prevent, and the same handlers
redirect when the Datastar-Request header is absent, so none of it requires
JavaScript. Posting with {contentType: 'form'} sends the enclosing form as
FormData, which means the handlers keep reading r.FormValue and no input had
to be rewritten as a signal.

The response is one SSE event carrying three elements: the list and both
forms. They have to move together — opening an edit form also has to clear a
delete that was mid-confirmation — and a text/html response can only replace
one element. The writer is twenty lines rather than re-adding the SDK and the
four modules it brings for a generator we would otherwise never call.

Smoke checks assert the wire format: that these answer with an event stream
carrying all three elements, that the deleted dish is absent from the patched
list, and that a header-less post still redirects.
2026-09-05 23:40:22 +03:00
Esa Kataja f8b908b39e Fix catalog redirects pointing at the old /ruoat
Adding, editing or deleting a dish redirected to /ruoat, which stopped
existing when the tab was renamed to Ruuat. Every one of those actions ended
on a 404. Shipped in v20260905-4.

The rename was done with a scripted replace across views.templ, main.go and
the smoke script; handlers.go was not in the list.

The tests did not catch it because they asserted only that the response was a
303. A redirect to a dead URL is still a 303. They now assert the target.
2026-09-05 23:35:45 +03:00
KessinenandEsa Kataja 174652778b Release: leftovers, the real dish list, and a day list that stays put (#2)
Tähteet — leftovers. Not a dish: it exists so a day can be recorded as "we ate what was already there" without inventing a meal nobody cooked. A special-flagged row created by migration 0002; never in the catalog, never editable, and excluded from the stage-2 suggester outright. It gets its own mark, a grey lidded tub — which exposed a bug where no categories drew the same icon as several.

The real dish list. The seed bundle is now your December 2024 list: 37 mains, 9 sides. Letut and Pannari dropped as not-dinners, Kanakintut folded into Broilerin koipireidet, has_sides assigned by rule.

The day list stays put. Choosing a day used to swap a panel in above the list and drop that day out of it, so rows below jumped up under the tap. The list is now the page; the selected day expands where it sits, with anchors so the viewport lands on the day rather than the top.

Release pipeline fixed. The last release silently pushed the previous image: release declared image and push as prerequisites and your make runs -j16, so they raced. Also, push re-derived the tag by date-sorting, which is ambiguous when two tags share a commit. It now reads what image recorded, and verifies afterwards that the registry serves what was built.

Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #2
2026-09-05 20:24:02 +00:00
KessinenandEsa Kataja e9754488db Release: one log page, grouped dishes, live search (#1)
Structure
- Kirjaa and Historia are one page. They were two views of the same thing — every history row already linked into the logger, and the logger had a day switcher. Two tabs instead of three. Also closed a gap: on an already-logged day there was no way to swap to a different dish, only to re-pick its sides.
- Ruoat → Ruuat, label and route.
- The catalog has a structure. It had no top-level headings at all — the mains simply began with "Liha". Both halves now carry a heading and a count, categories are visibly subordinate, and the add/edit forms collapse instead of filling the screen before any content.

Finding things
- Dishes grouped by category on both screens, Sekalaiset for multi-category ones. Derived from the stored set, not a fifth category, so one Tortillat still covers all four for the §8.1 suggester later.
- Live search on both lists, 250 ms after typing stops. Both remain plain GET forms, so they still filter with JavaScript off.
- History is paged 30 days at a time — it previously rendered every day back to the first entry, forever.

Correctness
- Future meals refused. The picker offered them and ?pvm= accepted them.
- today() wasn't midnight, so it never equalled a date parsed from ?pvm= — after saving, the card read "la 5.9. kirjattu" instead of "Tänään kirjattu".
- Deletes ask first, for dishes and logged meals. The meal is the more destructive: a dish is only soft-deleted.
- DB open failures name the path and uid, instead of unable to open database file (14).

Visual
- Category icons replace colour dots — steak, drumstick, fish, leaf, quartered circle.
- Row actions are a pencil and a bin; the header has a surface.

Housekeeping
- Datastar SDK dropped — one JSON decode was pulling in four modules including an HTTP compression stack. Five lines replace it.
- Release policy documented: main protected, releases arrive as PRs.

Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #1
2026-09-05 19:28:54 +00:00
Esa Kataja 15495ec0c0 Add a header, a theme toggle and visible checkboxes
Three pieces of chrome that were missing or misleading.

A header with the bowl mark and the app name, on every page. The page title
bar below it is no longer sticky: on a phone a tall sticky header eats the
screen, and the bottom tab bar already handles navigation.

A theme toggle, remembered per device in localStorage, because a shared
instance with no accounts should let the kitchen phone and the laptop
disagree. Dark by default, and the default lives in the server-rendered
markup so it survives with JavaScript off and never flashes. The button shows
the theme that is on — moon while dark, sun while light — rather than the one
a click would bring, and its label names the state before the action. Both
icons ship on every page and CSS picks one, so the server never needs to know
what this device chose. Following the system was considered and dropped: a
third state costs a control harder to read than the choice is worth.

The checkbox chips no longer hide the native control behind opacity: 0. With
only a background colour to go on there was no way to tell "Tarjoillaan
lisukkeiden kanssa" on from off. Colour is not a state indicator; a checkbox
is. This covers the side, category and has_sides chips alike.

Smoke checks cover the parts that would regress silently: that dark is the
no-JS default, and that both theme icons are present for CSS to choose from.
2026-09-05 19:05:41 +03:00
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
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
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
Esa Kataja c9a63bdd9e 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.
2026-09-05 18:15:17 +03:00