A release spans a fix, a feature and some chores at once, so no single
conventional type describes it honestly. release: makes main's log one line
per deployment, which is what that branch is for, with the pull request body
as the notes.
A dozen conventions had accumulated that existed only in my head and in the
commit log: Finnish interface against English code, no infrastructure detail
in the repository, migrations immutable once shipped, interactions that patch
rather than navigate, ponytail comments marking deliberate shortcuts, and
asserting what a response does rather than only that it responded — the one
that let the /ruoat 404 ship.
Commit messages take a type from here on: feat, fix, chore and the rest. The
body still carries the weight, since these commits are the only design record
this project has. Pull request titles get the same treatment, because main is
squash-merged and a title becomes a commit message on it.
With dev and prod open side by side in the same browser, the tabs were
indistinguishable. ENV is written into the title of every page unless it says
prod, so "dev · Foodster" picks itself out. The value is used verbatim, so
ENV=staging labels itself too, and prod and production both count as unmarked
so a stray capital cannot tag the real instance.
Environment variables lose their prefix: PASSWORD, DB, ENV, ADDR, HOST, REPO,
TAG. The container namespaces them already, and this matches how the other
services here are configured.
PUID/PGID are the exception rather than UID/GID. UID is read-only in bash, so
a value set in .env would be silently replaced by the invoking shell's own and
compose's user: would ignore what was asked for.
Breaking for a running instance: the deployed .env has to be rewritten in the
same deploy, or the app will refuse to start on an unset PASSWORD.
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.
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.
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.