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.
Every household now always has exactly one or more admins. Admins can
invite new members (previously open to any parent -- now gated), promote/
demote other admins, remove parents, and directly set another parent's
password (no email infra exists for a reset-link flow, so this is a
direct admin-sets-the-value action). Every parent can change their own
password with current-password confirmation. A sole admin can't remove
themselves or demote until they promote someone else -- this falls out of
a single "household must have >=1 admin" invariant rather than needing
special-case code.
Schema: parents.is_admin, added via a new idempotent ensureColumn() helper
(SQLite has no ADD COLUMN IF NOT EXISTS, and this needed to run safely
against the already-populated production parents table on next boot, not
just fresh installs). A boot-time backfill promotes the earliest-created
parent in any household with zero admins -- verified against a simulated
copy of the real production schema/data (including the exact "spouse
joined via invite" scenario), confirming correct promotion and clean
idempotency across repeated boots.
Fixed a real foreign-key landmine along the way: household_invites.
used_by_parent_id had no ON DELETE clause, so deleting any parent who'd
ever accepted an invite -- i.e. any spouse, in this app's normal usage --
would have thrown a constraint violation. src/lib/removeParent.js nulls
that reference before deleting, wrapped in an explicit transaction (first
use of manual BEGIN/COMMIT/ROLLBACK in this codebase, verified working
with node:sqlite before relying on it).
Verified extensively: every route's permission/edge cases via curl
(cross-household isolation, sole-admin guards, password round-trips via
real login), and the full UI flow (promote/demote/remove/both password
modals/leave-with-error-toast) across two independent real browser
sessions acting as admin and non-admin simultaneously. Full-app regression
and a Docker build/boot check both pass with the new code in place.
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.
iOS decides whether "Add to Home Screen" creates a true standalone app
(no browser chrome) or a plain bookmark-style Web Clip (full Safari UI --
URL bar, share/reload/compass row, X to close) based on whatever page you
were on when you tapped it. Those tags only existed on dashboard.html and
calendar.html, so anyone adding the icon while logged out -- the normal
first-time flow, since you land on /login.html before signing in -- got
the bookmark-style icon instead of a real standalone app, exactly as
reported. Existing home-screen icons need to be deleted and re-added
after this deploys; iOS bakes in standalone-capability at creation time
and won't retroactively upgrade an icon that already exists.
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.