# 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/`) 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 `). 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). ```bash 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: ```bash 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: ```bash 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 ```bash git add 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`.