Compare commits
4
Commits
dev
...
00b737cf8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b737cf8d | ||
|
|
0f0f04240a | ||
|
|
2695b8c01b | ||
|
|
85bf5e1a25 |
@@ -0,0 +1,8 @@
|
|||||||
|
name: demo
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo "hello from ${{ gitea.repository }} on ${{ gitea.ref }}"
|
||||||
@@ -75,6 +75,16 @@ title — the CalVer tag is not created until `make image` runs after the merge.
|
|||||||
|
|
||||||
The types above are for `dev`, where a commit really does do one thing.
|
The types above are for `dev`, where a commit really does do one thing.
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
The server keeps its own `compose.yaml` and `.env`. Neither is pulled from
|
||||||
|
here, so a release that renames a variable, adds one, or changes a mount
|
||||||
|
needs both copied across **in the same deploy** — otherwise the container
|
||||||
|
comes up against the old names and the app refuses to start.
|
||||||
|
|
||||||
|
Anything in this repository that reaches the server by hand belongs in the
|
||||||
|
release notes, flagged as breaking.
|
||||||
|
|
||||||
## Things that are easy to get wrong
|
## Things that are easy to get wrong
|
||||||
|
|
||||||
- **The interface is Finnish.** Code, comments, this file and the PRD are
|
- **The interface is Finnish.** Code, comments, this file and the PRD are
|
||||||
|
|||||||
@@ -60,12 +60,19 @@ func render(w http.ResponseWriter, r *http.Request, c templ.Component) {
|
|||||||
// there is nothing to write down for a dinner that has not happened, and a
|
// there is nothing to write down for a dinner that has not happened, and a
|
||||||
// stray entry dated next year would sit at the top of the history forever.
|
// stray entry dated next year would sit at the top of the history forever.
|
||||||
// Every read and write goes through here, so the clamp covers them all.
|
// Every read and write goes through here, so the clamp covers them all.
|
||||||
|
//
|
||||||
|
// The past is clamped too, at maxHistoryDays. The day list runs unbroken from
|
||||||
|
// today down to the selected day, so a picker set to 1994 would ask for eleven
|
||||||
|
// thousand rows. Same ceiling ?paivat= already has.
|
||||||
func (a *app) date(r *http.Request) time.Time {
|
func (a *app) date(r *http.Request) time.Time {
|
||||||
now := today(a.loc)
|
now := today(a.loc)
|
||||||
if raw := r.FormValue("pvm"); raw != "" {
|
if raw := r.FormValue("pvm"); raw != "" {
|
||||||
if d, err := time.ParseInLocation(dateLayout, raw, a.loc); err == nil {
|
if d, err := time.ParseInLocation(dateLayout, raw, a.loc); err == nil {
|
||||||
if d.After(now) {
|
switch floor := now.AddDate(0, 0, -maxHistoryDays+1); {
|
||||||
|
case d.After(now):
|
||||||
return now
|
return now
|
||||||
|
case d.Before(floor):
|
||||||
|
return floor
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
@@ -205,15 +212,13 @@ func (a *app) buildLog(r *http.Request, o logOptions) logView {
|
|||||||
// list underneath the tap.
|
// list underneath the tap.
|
||||||
func (a *app) loadDays(r *http.Request, v *logView) {
|
func (a *app) loadDays(r *http.Request, v *logView) {
|
||||||
v.HistoryDays = historyWindow(r)
|
v.HistoryDays = historyWindow(r)
|
||||||
|
|
||||||
// The window has to reach the selected day, or it would have nowhere to
|
|
||||||
// expand.
|
|
||||||
if reach := int(v.Today.Sub(v.Date).Hours()/24) + 1; reach > v.HistoryDays {
|
|
||||||
v.HistoryDays = min(reach, maxHistoryDays)
|
|
||||||
}
|
|
||||||
v.HistoryMore = v.HistoryDays + historyDays
|
v.HistoryMore = v.HistoryDays + historyDays
|
||||||
|
|
||||||
page, err := history(a.db, a.loc, v.Today, v.HistoryDays)
|
// The window has to reach the selected day, or it would have nowhere to
|
||||||
|
// expand. history() takes it as a floor rather than the caller inflating
|
||||||
|
// the day count, because the window also truncates at the first entry ever
|
||||||
|
// logged — and a day older than that still has to be loggable.
|
||||||
|
page, err := history(a.db, a.loc, v.Today, v.HistoryDays, v.Date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("history: %v", err)
|
log.Printf("history: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,10 +100,15 @@ func run() error {
|
|||||||
// local instance can pick another port.
|
// local instance can pick another port.
|
||||||
addr := cmp.Or(os.Getenv("ADDR"), listenAddr)
|
addr := cmp.Or(os.Getenv("ADDR"), listenAddr)
|
||||||
|
|
||||||
|
// WriteTimeout and IdleTimeout matter more than they look: the pool holds
|
||||||
|
// exactly one database connection, so a reader stalling on a long history
|
||||||
|
// response blocks every other request behind it.
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: routes(db, loc, password),
|
Handler: routes(db, loc, password),
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
+96
-7
@@ -305,7 +305,8 @@ func updateMain(db *sql.DB, id int64, name string, categories []string, hasSides
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
if _, err := tx.Exec(
|
if _, err := tx.Exec(
|
||||||
`UPDATE main_dishes SET name = ?, has_sides = ? WHERE id = ? AND deleted_at IS NULL`,
|
`UPDATE main_dishes SET name = ?, has_sides = ?
|
||||||
|
WHERE id = ? AND deleted_at IS NULL AND special = 0`,
|
||||||
name, hasSides, id,
|
name, hasSides, id,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return taken(err)
|
return taken(err)
|
||||||
@@ -345,9 +346,14 @@ func updateSide(db *sql.DB, id int64, name string) error {
|
|||||||
|
|
||||||
// Soft delete: the row stays so historical log entries keep resolving their
|
// Soft delete: the row stays so historical log entries keep resolving their
|
||||||
// names, but it disappears from the catalog and every picker (PRD §6).
|
// names, but it disappears from the catalog and every picker (PRD §6).
|
||||||
|
//
|
||||||
|
// `special = 0` here and in updateMain: the catalog never lists Tähteet, so
|
||||||
|
// the UI cannot reach it, but a stale tab or a hand-made POST could — and
|
||||||
|
// removing it would take away the row migration 0002 guarantees.
|
||||||
func softDeleteMain(db *sql.DB, id int64) error {
|
func softDeleteMain(db *sql.DB, id int64) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
`UPDATE main_dishes SET deleted_at = datetime('now') WHERE id = ?`, id)
|
`UPDATE main_dishes SET deleted_at = datetime('now')
|
||||||
|
WHERE id = ? AND special = 0`, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +438,12 @@ type HistoryPage struct {
|
|||||||
// history walks back day by day from a given day, so a day nobody wrote down
|
// history walks back day by day from a given day, so a day nobody wrote down
|
||||||
// shows up as an explicit gap rather than silently missing. It stops at the
|
// shows up as an explicit gap rather than silently missing. It stops at the
|
||||||
// first entry ever recorded — before that there is no history to be missing.
|
// first entry ever recorded — before that there is no history to be missing.
|
||||||
func history(db *sql.DB, loc *time.Location, from time.Time, days int) (HistoryPage, error) {
|
//
|
||||||
|
// reach names a day that must appear whatever the window says. The log board
|
||||||
|
// opens inside the selected day's row, so a date picked from before the first
|
||||||
|
// entry ever logged used to render nothing at all: no row, no board, no way to
|
||||||
|
// log it. Pass the zero time to ask for the plain window.
|
||||||
|
func history(db *sql.DB, loc *time.Location, from time.Time, days int, reach time.Time) (HistoryPage, error) {
|
||||||
var first sql.NullString
|
var first sql.NullString
|
||||||
if err := db.QueryRow(`SELECT min(date) FROM meal_log`).Scan(&first); err != nil {
|
if err := db.QueryRow(`SELECT min(date) FROM meal_log`).Scan(&first); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@@ -457,14 +468,92 @@ func history(db *sql.DB, loc *time.Location, from time.Time, days int) (HistoryP
|
|||||||
oldest = firstDate
|
oldest = firstDate
|
||||||
page.More = false
|
page.More = false
|
||||||
}
|
}
|
||||||
|
if !reach.IsZero() && reach.Before(oldest) {
|
||||||
|
oldest = reach
|
||||||
|
page.More = firstDate.Before(oldest)
|
||||||
|
}
|
||||||
page.Next = oldest.AddDate(0, 0, -1)
|
page.Next = oldest.AddDate(0, 0, -1)
|
||||||
|
|
||||||
|
entries, err := entriesBetween(db, oldest, from)
|
||||||
|
if err != nil {
|
||||||
|
return HistoryPage{}, err
|
||||||
|
}
|
||||||
for d := from; !d.Before(oldest); d = d.AddDate(0, 0, -1) {
|
for d := from; !d.Before(oldest); d = d.AddDate(0, 0, -1) {
|
||||||
entry, err := entryFor(db, d)
|
row := HistoryRow{Date: d}
|
||||||
if err != nil {
|
if e := entries[d.Format(dateLayout)]; e != nil {
|
||||||
return HistoryPage{}, err
|
e.Date = d
|
||||||
|
row.Entry = e
|
||||||
}
|
}
|
||||||
page.Rows = append(page.Rows, HistoryRow{Date: d, Entry: entry})
|
page.Rows = append(page.Rows, row)
|
||||||
}
|
}
|
||||||
return page, nil
|
return page, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// entriesBetween loads every logged day in the inclusive range, keyed by
|
||||||
|
// stored date string, in two queries rather than two per day. The widest
|
||||||
|
// window a URL can ask for is five years, which day-at-a-time made 3,600 round
|
||||||
|
// trips through a pool of exactly one connection.
|
||||||
|
func entriesBetween(db *sql.DB, from, to time.Time) (map[string]*Entry, error) {
|
||||||
|
lo, hi := from.Format(dateLayout), to.Format(dateLayout)
|
||||||
|
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT l.id, l.date, m.id, m.name, m.has_sides,
|
||||||
|
coalesce((SELECT group_concat(c.category)
|
||||||
|
FROM main_dish_categories c
|
||||||
|
WHERE c.main_dish_id = m.id), '')
|
||||||
|
FROM meal_log l
|
||||||
|
JOIN main_dishes m ON m.id = l.main_dish_id
|
||||||
|
WHERE l.date BETWEEN ? AND ?`, lo, hi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
byDate := map[string]*Entry{}
|
||||||
|
byLog := map[int64]*Entry{}
|
||||||
|
for rows.Next() {
|
||||||
|
var logID int64
|
||||||
|
var date, cats string
|
||||||
|
var e Entry
|
||||||
|
if err := rows.Scan(
|
||||||
|
&logID, &date, &e.Main.ID, &e.Main.Name, &e.Main.HasSides, &cats,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cats != "" {
|
||||||
|
e.Main.Categories = strings.Split(cats, ",")
|
||||||
|
}
|
||||||
|
byDate[date] = &e
|
||||||
|
byLog[logID] = &e
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(byLog) == 0 {
|
||||||
|
return byDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sides, err := db.Query(`
|
||||||
|
SELECT ls.meal_log_id, s.id, s.name
|
||||||
|
FROM meal_log_sides ls
|
||||||
|
JOIN side_dishes s ON s.id = ls.side_dish_id
|
||||||
|
JOIN meal_log l ON l.id = ls.meal_log_id
|
||||||
|
WHERE l.date BETWEEN ? AND ?
|
||||||
|
ORDER BY s.name`, lo, hi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer sides.Close()
|
||||||
|
|
||||||
|
for sides.Next() {
|
||||||
|
var logID int64
|
||||||
|
var s Side
|
||||||
|
if err := sides.Scan(&logID, &s.ID, &s.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if e := byLog[logID]; e != nil {
|
||||||
|
e.Sides = append(e.Sides, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byDate, sides.Err()
|
||||||
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ func TestHistoryMarksUnloggedDaysAsGaps(t *testing.T) {
|
|||||||
t.Fatalf("save -3: %v", err)
|
t.Fatalf("save -3: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
page, err := history(h.db, loc, now, 60)
|
page, err := history(h.db, loc, now, 60, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("history: %v", err)
|
t.Fatalf("history: %v", err)
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ func TestHistoryPagesInWindows(t *testing.T) {
|
|||||||
t.Fatalf("save -9: %v", err)
|
t.Fatalf("save -9: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
first, err := history(h.db, loc, now, 5)
|
first, err := history(h.db, loc, now, 5, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first window: %v", err)
|
t.Fatalf("first window: %v", err)
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ func TestHistoryPagesInWindows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The windows must meet exactly: no day repeated, none skipped.
|
// The windows must meet exactly: no day repeated, none skipped.
|
||||||
second, err := history(h.db, loc, first.Next, 5)
|
second, err := history(h.db, loc, first.Next, 5, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second window: %v", err)
|
t.Fatalf("second window: %v", err)
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,7 @@ func TestHistoryPagesInWindows(t *testing.T) {
|
|||||||
func TestHistoryEmptyWithoutEntries(t *testing.T) {
|
func TestHistoryEmptyWithoutEntries(t *testing.T) {
|
||||||
h := seeded(t)
|
h := seeded(t)
|
||||||
|
|
||||||
page, err := history(h.db, time.UTC, today(time.UTC), 60)
|
page, err := history(h.db, time.UTC, today(time.UTC), 60, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("history: %v", err)
|
t.Fatalf("history: %v", err)
|
||||||
}
|
}
|
||||||
@@ -316,3 +316,64 @@ func TestHistoryEmptyWithoutEntries(t *testing.T) {
|
|||||||
t.Error("More is set although there is no history at all")
|
t.Error("More is set although there is no history at all")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A date picked from before the first entry ever logged used to fall outside
|
||||||
|
// the window entirely: no row, so the log board had nothing to open in and the
|
||||||
|
// day could not be filled in at all.
|
||||||
|
func TestHistoryReachesDaysOlderThanTheFirstEntry(t *testing.T) {
|
||||||
|
h := seeded(t)
|
||||||
|
loc := time.UTC
|
||||||
|
now := today(loc)
|
||||||
|
|
||||||
|
if err := saveEntry(h.db, now, h.mainNamed(t, "Lohikeitto"), nil); err != nil {
|
||||||
|
t.Fatalf("save today: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := now.AddDate(0, 0, -100)
|
||||||
|
page, err := history(h.db, loc, now, 30, want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("history: %v", err)
|
||||||
|
}
|
||||||
|
if len(page.Rows) != 101 {
|
||||||
|
t.Fatalf("%d rows, want 101 (today back to the selected day)", len(page.Rows))
|
||||||
|
}
|
||||||
|
last := page.Rows[len(page.Rows)-1]
|
||||||
|
if !last.Date.Equal(want) {
|
||||||
|
t.Errorf("last row is %s, want the selected %s",
|
||||||
|
last.Date.Format(dateLayout), want.Format(dateLayout))
|
||||||
|
}
|
||||||
|
if page.More {
|
||||||
|
t.Error("More is set although the window reached past the oldest entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sides of every day come back in one query now; each still has to land on
|
||||||
|
// its own day.
|
||||||
|
func TestHistoryKeepsSidesWithTheirOwnDay(t *testing.T) {
|
||||||
|
h := seeded(t)
|
||||||
|
loc := time.UTC
|
||||||
|
now := today(loc)
|
||||||
|
|
||||||
|
muusi := h.sideNamed(t, "Perunamuusi")
|
||||||
|
riisi := h.sideNamed(t, "Riisi")
|
||||||
|
if err := saveEntry(h.db, now, h.mainNamed(t, "Lohikeitto"), []int64{muusi}); err != nil {
|
||||||
|
t.Fatalf("save today: %v", err)
|
||||||
|
}
|
||||||
|
if err := saveEntry(h.db, now.AddDate(0, 0, -1), h.mainNamed(t, "Lihapullat"), []int64{riisi}); err != nil {
|
||||||
|
t.Fatalf("save -1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := history(h.db, loc, now, 30, time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("history: %v", err)
|
||||||
|
}
|
||||||
|
if len(page.Rows) != 2 {
|
||||||
|
t.Fatalf("%d rows, want 2", len(page.Rows))
|
||||||
|
}
|
||||||
|
for i, want := range []string{"Perunamuusi", "Riisi"} {
|
||||||
|
got := page.Rows[i].Entry
|
||||||
|
if got == nil || len(got.Sides) != 1 || got.Sides[0].Name != want {
|
||||||
|
t.Errorf("row %d sides = %+v, want just %s", i, got.Sides, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+20
-12
@@ -11,6 +11,12 @@ cd "$(dirname "$0")/.."
|
|||||||
|
|
||||||
addr=127.0.0.1:8099
|
addr=127.0.0.1:8099
|
||||||
pass=smoke
|
pass=smoke
|
||||||
|
|
||||||
|
# Dates are relative, never literal. A hardcoded one turns into "some day in
|
||||||
|
# the past" at the next midnight, and the assertions quietly start meaning
|
||||||
|
# something else.
|
||||||
|
d0=$(date +%F)
|
||||||
|
d1=$(date -d yesterday +%F)
|
||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
trap 'kill ${srv:-0} 2>/dev/null || true; rm -rf "$tmp"' EXIT
|
trap 'kill ${srv:-0} 2>/dev/null || true; rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
@@ -134,51 +140,53 @@ check "picking a dish opens the sides step" \
|
|||||||
|
|
||||||
check "saving redirects back to the day" \
|
check "saving redirects back to the day" \
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
||||||
-d "pvm=2026-09-05&ruoka=$ruoka" "http://$addr/kirjaa")" "303"
|
-d "pvm=$d0&ruoka=$ruoka" "http://$addr/kirjaa")" "303"
|
||||||
|
|
||||||
check "the saved day shows what was eaten" \
|
check "the saved day shows what was eaten" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "kirjattu"
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=$d0")" "kirjattu"
|
||||||
|
|
||||||
# The selected day expands inside the list rather than in a panel above it,
|
# The selected day expands inside the list rather than in a panel above it,
|
||||||
# so the rows below do not shift when one is tapped.
|
# so the rows below do not shift when one is tapped.
|
||||||
day=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")
|
day=$(curl -s -u ":$pass" "http://$addr/?pvm=$d0")
|
||||||
check "the selected day expands in place" "$day" 'class="open"'
|
check "the selected day expands in place" "$day" 'class="open"'
|
||||||
check "and stays in the list rather than being lifted out" "$day" "kirjattu"
|
check "and stays in the list rather than being lifted out" "$day" "kirjattu"
|
||||||
|
|
||||||
# ---- the day list patches in place instead of navigating ----------------
|
# ---- the day list patches in place instead of navigating ----------------
|
||||||
|
|
||||||
dayp=$(curl -s -u ":$pass" -H 'Datastar-Request: true' "http://$addr/paiva?pvm=2026-09-05")
|
dayp=$(curl -s -u ":$pass" -H 'Datastar-Request: true' "http://$addr/paiva?pvm=$d0")
|
||||||
check "opening a day patches the list" "$dayp" 'id="paivat"'
|
check "opening a day patches the list" "$dayp" 'id="paivat"'
|
||||||
refute "and returns a fragment, not a page" "$dayp" "<html"
|
refute "and returns a fragment, not a page" "$dayp" "<html"
|
||||||
|
|
||||||
check "picking a dish patches to the sides step" \
|
check "picking a dish patches to the sides step" \
|
||||||
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
||||||
"http://$addr/paiva?pvm=2026-09-05&ruoka=$ruoka")" "Tallenna"
|
"http://$addr/paiva?pvm=$d0&ruoka=$ruoka")" "Tallenna"
|
||||||
|
|
||||||
check "saving from Datastar patches back" \
|
check "saving from Datastar patches back" \
|
||||||
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
||||||
-d "pvm=2026-09-04&ruoka=$ruoka" "http://$addr/kirjaa")" 'id="paivat"'
|
-d "pvm=$d1&ruoka=$ruoka" "http://$addr/kirjaa")" 'id="paivat"'
|
||||||
|
|
||||||
check "deleting from Datastar patches back" \
|
check "deleting from Datastar patches back" \
|
||||||
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
"$(curl -s -u ":$pass" -H 'Datastar-Request: true' \
|
||||||
-d "pvm=2026-09-04" "http://$addr/poista")" 'id="paivat"'
|
-d "pvm=$d1" "http://$addr/poista")" 'id="paivat"'
|
||||||
|
|
||||||
# Without the header it must still redirect, for no JavaScript.
|
# Without the header it must still redirect, for no JavaScript.
|
||||||
check "a plain save still redirects to the day" \
|
check "a plain save still redirects to the day" \
|
||||||
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
||||||
-d "pvm=2026-09-04&ruoka=$ruoka" "http://$addr/kirjaa")" "pvm=2026-09-04"
|
-d "pvm=$d1&ruoka=$ruoka" "http://$addr/kirjaa")" "pvm=$d1"
|
||||||
|
|
||||||
# Deleting a logged meal drops the row outright, so it asks first.
|
# 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")
|
saved=$(curl -s -u ":$pass" "http://$addr/?pvm=$d0&poista=1")
|
||||||
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
|
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
|
||||||
refute "and does not delete while asking" "$saved" "Ei merkintää"
|
# Assert the entry is still shown, rather than that no gap row exists anywhere
|
||||||
|
# on the page: other days are legitimately unlogged and render their own.
|
||||||
|
check "and the entry is still there while asking" "$saved" "kirjattu"
|
||||||
|
|
||||||
check "deleting redirects back" \
|
check "deleting redirects back" \
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
||||||
-d "pvm=2026-09-05" "http://$addr/poista")" "303"
|
-d "pvm=$d0" "http://$addr/poista")" "303"
|
||||||
|
|
||||||
check "the day is empty again" \
|
check "the day is empty again" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "Etsi"
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=$d0")" "Etsi"
|
||||||
|
|
||||||
check "search filters the board" \
|
check "search filters the board" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
||||||
|
|||||||
Reference in New Issue
Block a user