Files
KCal/takeover.md
ort 73a1d58838 Add takeover.md
Project overview, current status, architecture summary, and the
commit/push workflow for the Gitea remote (deploy key location,
GIT_SSH_COMMAND usage).
2026-08-15 13:48:01 -04:00

7.8 KiB

Kids Calendar — Takeover Notes

What this is

A self-hosted family weekly calendar, evolved from a single static HTML file (weekly_schedule.html, still sitting untouched one directory up, kept as a design reference — not part of this app).

Three roles:

  • Parents — real accounts (email + password), grouped into a "household." Both parents in a household see and edit the same children and calendars. A parent invites their spouse via a one-time link rather than open signup into an existing household.
  • Children — no accounts. Each child gets an unguessable link (/k/<kiosk_token>) meant to be opened on a tablet. That view is read-only except for checkboxes — there is no server route that lets a kiosk link edit text, add/delete tasks, or see another child's data. The access boundary is enforced by which API routes exist, not by hiding UI.
  • Checkbox taps on the kiosk show up in the parent's browser within ~4 seconds via polling, without a manual reload.

Each child can have multiple calendars (e.g. "Week of Aug 17"), each independently saved/reopened/edited/duplicated/printed — not one calendar that just gets overwritten every week.

Status

Fully built and manually verified end-to-end (browser-driven testing, not an automated test suite — see "Not done" below). All planned phases are complete:

  1. Scaffold (Express + SQLite)
  2. Auth + household (signup/login/logout, sessions)
  3. Spouse invite flow
  4. Children + calendar CRUD, dashboard, calendar editor
  5. Kiosk view
  6. Polling for near-real-time sync
  7. Packaging (Docker) + hardening (rate limits, input length caps, CSRF)

Verified specifically:

  • Full parent flow: signup → invite spouse → add child → create calendar → edit tasks/labels → duplicate week (checks reset, tasks carry over) → print → reload persists everything.
  • Kiosk boundary: checkbox toggle works; direct API calls attempting to edit text/add/delete tasks through the kiosk token return 404 (no such route); bogus tokens 404; unauthenticated parent routes 401; cross-origin POSTs blocked (CSRF check) 403.
  • Polling: kiosk checkbox change reflected on parent view without reload; parent's in-progress edit is not clobbered by an incoming poll (polling pauses while any contenteditable field has focus, resumes on blur).
  • Docker: docker build, plain docker run, and docker compose up -d --build all tested directly — image builds with no native compile step, container survives a restart with data intact, compose's named volume and .env wiring both work as documented in README.md.

Not done / worth knowing

  • No automated test suite (unit/integration tests) — all verification so far was manual (curl scripts + the in-app browser tool). If this grows, that's the first gap to close.
  • No production deployment behind a real reverse proxy/HTTPS has been tested — COOKIE_SECURE and the reverse-proxy notes in README.md are written but unverified against a live TLS setup.
  • Git author identity on existing commits was auto-filled by git from the machine's username/hostname (Jorge Ortega II <ort84@Jorges-MacBook-Pro-2.local>). Reset with git config --global user.name / user.email if that's wrong, then optionally git commit --amend --reset-author on affected commits.

See README.md for full run instructions (local + Docker) and the environment variable reference — this file is about repo/process context, README.md is about running the app.

Architecture at a glance

  • Backend: Node.js + Express, src/app.js wires everything up. server.js is the entry point.
  • DB: SQLite via the built-in node:sqlite module (not better-sqlite3 — that failed to compile natively in this environment, and node:sqlite has the added benefit of needing zero native build tooling anywhere, including on a NAS/ARM host). Requires Node ≥22.5. Schema in src/db/schema.sql, applied idempotently on boot by src/db/migrate.js.
  • Sessions: custom SQLite-backed session store (src/lib/sqliteSessionStore.js) instead of connect-sqlite3, because that package pulls in the native sqlite3 module — same reason as above, avoid native deps entirely.
  • Auth: bcryptjs (pure JS) for password hashing, express-session for cookies. CSRF is handled by a same-origin Origin/Referer check (src/middleware/csrf.js) rather than a token library — reasonable for a small LAN-scale app, called out in README.md as worth upgrading if ever exposed to the wider internet.
  • Routes: src/routes/*.js, one file per resource area (auth, household, invites, children, calendars, kiosk). The kiosk router is mounted separately with no session middleware at all (app.use('/api/kiosk/:token', kioskRoutes)), authenticated purely by the token in the URL.
  • Frontend: plain HTML/CSS/vanilla JS in public/, no build step, served via express.static. public/js/calendarRender.js is the shared board-rendering logic used by both the parent editor (calendar.html/calendar.js, editable: true) and the kiosk view (kiosk.html/kiosk.js, editable: false) — one renderer, one editable flag gates every mutation affordance.
  • Data model: household → parents / children → calendars → calendar_blocks (per-calendar block labels) + calendar_tasks (per day+block). See src/db/schema.sql for the authoritative shape.

How to commit / push to Gitea

Remote: git@git.oservr.com:ort/KCal.git, port 23 (non-standard — that's why the remote is configured as a full ssh:// URL rather than the usual git@host:path shorthand, since the shorthand can't carry a custom port).

git remote -v
# origin  ssh://git@git.oservr.com:23/ort/KCal.git (fetch)
# origin  ssh://git@git.oservr.com:23/ort/KCal.git (push)

The deploy key

Lives in GitOServr/ in this project (note the spelling — no "e" before "r"):

  • GitOServr/GitOservr — the private key (ED25519). Never commit this. GitOServr/ is listed in .gitignore specifically to prevent that, but that's a safety net, not a substitute for being careful — e.g. don't run git add -f on it.
  • GitOServr/GitOservr.pub — the matching public key.
  • GitOServr/known_hosts — the Gitea host's SSH host keys, pinned via ssh-keyscan -p 23 git.oservr.com the first time this was set up (TOFU — trust on first use). If the server's host key ever legitimately changes (e.g. server migration), you'll need to re-run that scan and confirm the new key out-of-band before trusting it again — don't just delete known_hosts to make a warning go away.
  • GitOServr/GitOServr_0x9ED086FFE7AE26B8_public.asc — a PGP public key, not used by the git push flow above (likely for commit signing, unused so far).

The private key is outside ~/.ssh/, so plain git push won't find it — every push needs GIT_SSH_COMMAND pointed at it explicitly:

GIT_SSH_COMMAND='ssh -i "GitOServr/GitOservr" -o UserKnownHostsFile="GitOServr/known_hosts"' git push

(Run from the kids-calendar/ directory, since the paths above are relative to it.)

If this gets tedious, the standing alternative — deliberately not set up, per a "no thanks" during initial setup — is a repo-local git config entry:

git config core.sshCommand 'ssh -i "GitOServr/GitOservr" -o UserKnownHostsFile="GitOServr/known_hosts"'

That makes plain git push/git pull work without the env var prefix. It's local to this repo's .git/config (never pushed, never affects other repos on this machine).

Normal workflow

git add <files>
git commit -m "..."
GIT_SSH_COMMAND='ssh -i "GitOServr/GitOservr" -o UserKnownHostsFile="GitOServr/known_hosts"' git push

Current state: 2 commits on main, both pushed — initial app commit, then a follow-up that added GitOServr/ and .DS_Store to .gitignore.