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
This commit was merged in pull request #1.
This commit is contained in:
2026-09-05 19:28:54 +00:00
co-authored by Esa Kataja
parent eb95bd0a03
commit e9754488db
13 changed files with 1194 additions and 264 deletions
+89 -20
View File
@@ -43,6 +43,16 @@ check() {
fi
}
# refute <name> <haystack> <needle>
refute() {
if printf '%s' "$2" | grep -qF -- "$3"; then
echo " FAIL $1 (should not contain: $3)"
fail=1
else
echo " ok $1"
fi
}
echo "smoke: http://$addr"
check "unauthenticated request is refused" \
@@ -75,29 +85,29 @@ check "manifest has the right content type" \
"application/manifest+json"
check "catalog starts empty" \
"$(curl -s -u ":$pass" "http://$addr/ruoat")" "0 pääruokaa"
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "0 pääruokaa"
out=$(curl -s -u ":$pass" -F "tiedosto=@seeds/testi.json" "http://$addr/ruoat/tuonti")
out=$(curl -s -u ":$pass" -F "tiedosto=@seeds/testi.json" "http://$addr/ruuat/tuonti")
check "file upload imports the seed bundle" "$out" "Lisätty 22, ohitettu 0"
check "counts update after import" "$out" "16 pääruokaa, 6 lisuketta"
check "re-import refuses duplicates" \
"$(curl -s -u ":$pass" -F "tiedosto=@seeds/testi.json" "http://$addr/ruoat/tuonti")" \
"$(curl -s -u ":$pass" -F "tiedosto=@seeds/testi.json" "http://$addr/ruuat/tuonti")" \
"jo listalla"
check "pasted JSON imports" \
"$(curl -s -u ":$pass" -F 'json={"mains":[],"sides":[{"name":"Perunasalaatti"}]}' \
"http://$addr/ruoat/tuonti")" "Lisätty 1"
"http://$addr/ruuat/tuonti")" "Lisätty 1"
check "unknown category is reported" \
"$(curl -s -u ":$pass" -F 'json={"mains":[{"name":"Rikki","categories":["kana"]}],"sides":[]}' \
"http://$addr/ruoat/tuonti")" "tuntematon kategoria"
"http://$addr/ruuat/tuonti")" "tuntematon kategoria"
check "empty submit is explained" \
"$(curl -s -u ":$pass" -F 'json=' "http://$addr/ruoat/tuonti")" "Ei tuotavaa"
"$(curl -s -u ":$pass" -F 'json=' "http://$addr/ruuat/tuonti")" "Ei tuotavaa"
check "malformed JSON is explained" \
"$(curl -s -u ":$pass" -F 'json={nope' "http://$addr/ruoat/tuonti")" "JSON ei kelpaa"
"$(curl -s -u ":$pass" -F 'json={nope' "http://$addr/ruuat/tuonti")" "JSON ei kelpaa"
# ---- the log flow, against the dishes imported above --------------------
@@ -122,8 +132,13 @@ check "saving redirects back to the day" \
check "the saved day shows what was eaten" \
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "kirjattu"
check "history lists the entry" \
"$(curl -s -u ":$pass" "http://$addr/historia")" "syyskuu"
check "history is on the same page as the logger" \
"$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin"
# Deleting a logged meal drops the row outright, so it asks first.
saved=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05&poista=1")
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
refute "and does not delete while asking" "$saved" "Ei merkintää"
check "deleting redirects back" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
@@ -135,6 +150,43 @@ check "the day is empty again" \
check "search filters the board" \
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
# ---- live search: Datastar sends signals as JSON in ?datastar= -----------
live=$(curl -s -u ":$pass" --get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")
check "live search returns the board fragment" "$live" 'id="lauta"'
check "live search applies the term" "$live" "keitto"
refute "live search excludes non-matches" "$live" "Lihapullat"
refute "the fragment is not a whole page" "$live" "<html"
check "live search is served as html for Datastar to patch" \
"$(curl -s -o /dev/null -w '%{content_type}' -u ":$pass" \
--get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")" \
"text/html"
cat_live=$(curl -s -u ":$pass" --get --data-urlencode 'datastar={"haku":"riisi"}' "http://$addr/ruuat/etsi")
check "catalog live search returns its fragment" "$cat_live" 'id="ruokalista"'
check "catalog live search matches sides too" "$cat_live" "Riisi"
refute "catalog live search excludes non-matches" "$cat_live" "Lihapullat"
# The plain form still works without JavaScript.
check "catalog search works as a plain form too" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?haku=riisi")" "Riisi"
# Nothing was eaten tomorrow. A future date is clamped rather than logged.
future=$(date -d '+30 days' +%Y-%m-%d)
check "a future date falls back to today" \
"$(curl -s -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
check "saving a future date is clamped too" \
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
-d "pvm=$future&ruoka=$ruoka" "http://$addr/kirjaa")" "/"
check "tomorrow was not written to the log" \
"$(curl -s -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
# Clean up the entry that clamped onto today.
curl -s -o /dev/null -u ":$pass" -d "pvm=$(date +%Y-%m-%d)" "http://$addr/poista"
# ---- adding a dish without leaving Kirjaa --------------------------------
miss=$(curl -s -u ":$pass" "http://$addr/?haku=Poronkariste")
@@ -157,37 +209,54 @@ check "the quick-added dish is on the board" \
check "adding a main redirects" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d 'nimi=uunikala&kategoria=fish&lisukkeita=1' "http://$addr/ruoat/paaruoka")" "303"
-d 'nimi=uunikala&kategoria=fish&lisukkeita=1' "http://$addr/ruuat/paaruoka")" "303"
catalog=$(curl -s -u ":$pass" "http://$addr/ruoat")
catalog=$(curl -s -u ":$pass" "http://$addr/ruuat")
check "the new main is listed, sentence-cased" "$catalog" "Uunikala"
check "a duplicate name is refused" \
"$(curl -s -u ":$pass" -d 'nimi=UUNIKALA&kategoria=fish' "http://$addr/ruoat/paaruoka")" \
"$(curl -s -u ":$pass" -d 'nimi=UUNIKALA&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
"Nimi on jo listalla."
check "a main with no category is refused" \
"$(curl -s -u ":$pass" -d 'nimi=Kategoriaton' "http://$addr/ruoat/paaruoka")" \
"$(curl -s -u ":$pass" -d 'nimi=Kategoriaton' "http://$addr/ruuat/paaruoka")" \
"Valitse vähintään yksi kategoria."
check "a nameless dish is refused" \
"$(curl -s -u ":$pass" -d 'nimi=+++&kategoria=fish' "http://$addr/ruoat/paaruoka")" \
"$(curl -s -u ":$pass" -d 'nimi=+++&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
"Anna nimi."
check "adding a side redirects" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d 'nimi=lohkoperunat' "http://$addr/ruoat/lisuke")" "303"
-d 'nimi=lohkoperunat' "http://$addr/ruuat/lisuke")" "303"
check "the new side is listed" \
"$(curl -s -u ":$pass" "http://$addr/ruoat")" "Lohkoperunat"
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Lohkoperunat"
uusi=$(printf '%s' "$catalog" | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2)
# The id of Uunikala specifically: the catalog is grouped and alphabetical, so
# the first id on the page belongs to some other dish entirely.
uusi=$(printf '%s' "$catalog" | grep -o 'Uunikala.*' | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2)
if [ -z "$uusi" ]; then
echo " FAIL could not find Uunikala's id in the catalog"
fail=1
uusi=0
fi
check "the edit form is prefilled" \
"$(curl -s -u ":$pass" "http://$addr/ruoat?muokkaa=$uusi")" "Muokkaa pääruokaa"
"$(curl -s -u ":$pass" "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa"
check "deleting a main redirects" \
# A bin icon is easy to hit by accident, so the row asks before anything goes.
check "the bin asks before deleting" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Poista?"
check "the dish is still there while it asks" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Uunikala"
check "confirming the delete redirects" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d "id=$uusi&tyyppi=paa" "http://$addr/ruoat/poista")" "303"
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303"
refute "the dish is gone once confirmed" \
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Uunikala"
if [ "$fail" -ne 0 ]; then
echo "smoke: FAILED"