Every task now has a duration (10 min default) and a child-initiated start:
a play icon on the kiosk, alongside the checkbox, begins a server-anchored
countdown for that specific task. Deliberately not a beep-every-minute
alarm -- audio is milestone-only (start/halfway/almost-done/time's-up),
since frequent interrupting alerts can backfire for ADHD kids rather than
help. Countdown is drift-free (recomputed each tick from the fixed
started_at anchor, never decremented), audio is synthesized client-side
via Web Audio (zero external assets, matching this app's existing
hand-rolled-PNG-icon philosophy), and the child's own start tap is what
unlocks audio playback for the rest of the session on iOS.
Two real bugs caught and fixed during design, before they shipped:
- Marking a task done now clears started_at (both the kiosk and parent
routes) -- without this, unchecking a finished task later would
resurrect a stale timer and instantly show "time's up" on a task the
child hasn't touched today.
- The parent editor's poll-pause guard only covered contenteditable
fields; a plain number input for duration would have been silently
unprotected from being clobbered by an incoming 4s poll mid-edit.
Extended the guard to cover it, verified live that the DOM and focus
both survive a poll while the field is focused.
Schema: calendar_tasks gains duration_minutes (NOT NULL DEFAULT 10) and
started_at (nullable). Verified against a simulated copy of the real
production schema/data that the static DEFAULT backfills every existing
row automatically (no business-logic backfill needed, unlike is_admin),
and that repeated boots stay idempotent. Duplicate-calendar carries
durations forward but always resets started_at, verified end-to-end.
Kiosk countdown/milestone-firing verified live in-browser end-to-end: real
timer start, halfway/almost-done/done firing at the correct proportional
thresholds (not fixed minutes, so it scales from ~1min to 20+min tasks),
and a mid-countdown page reload resuming at the correct remaining time
with already-passed milestones backfilled silently rather than replayed.
The kiosk deliberately ignored calendar.showWeekend when it was built,
treating the parent's toggle as print-only -- but that's not what parents
actually expect: hiding the weekend on the dashboard should hide it on
the tablet too. kiosk.js now toggles weekendWrap's hidden class the same
way the parent editor does.
Separately, weekdayBoard's auto-fit/minmax(240px) grid only fit 4 columns
on real tablet landscape widths (~1024-1180px), pushing Friday to a
second row. Since weekdayBoard always holds exactly 5 day-cards, forcing
repeat(5, 1fr) once there's clearly tablet-landscape width (960px+,
excludes large phones in landscape) fits all 5 comfortably instead of
relying on auto-fit's width heuristic.
Verified both at real tablet landscape sizes (1024x768, 1180x820) with
the Browser tool: 5 weekday columns fit in one row, and the weekend
section correctly appears/disappears with showWeekend.
Parents get a real push notification when a kid checks off a task
(false->true transitions only, fire-and-forget, degrades gracefully with
no VAPID keys configured). Dashboard is a fully installable iOS/Android
PWA; each child's kiosk link gets its own dynamic per-token manifest so
"Add to Home Screen" opens straight into their board in standalone mode.
Kiosk view is reworked for tablets: safe-area-aware full-bleed layout,
the whole task row is now tappable (previously only the 24px checkbox
was, well under Apple's touch-target minimum), and app icons are
generated by a small dependency-free PNG encoder (no image tooling
available in this environment).
Push requires real HTTPS (iOS Safari won't allow it otherwise) - README
and UNRAID.md cover VAPID setup and the HTTPS prerequisite.
Express + SQLite (node:sqlite, no native build step) family calendar:
parent accounts with household invites, per-child calendars with
save/duplicate/print, a token-gated read-only kiosk view for tablets,
and polling to keep parent and kiosk views in sync. Defaults to port 3007.