Project overview, current status, architecture summary, and the commit/push workflow for the Gitea remote (deploy key location, GIT_SSH_COMMAND usage).
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:
- Scaffold (Express + SQLite)
- Auth + household (signup/login/logout, sessions)
- Spouse invite flow
- Children + calendar CRUD, dashboard, calendar editor
- Kiosk view
- Polling for near-real-time sync
- 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
contenteditablefield has focus, resumes on blur). - Docker:
docker build, plaindocker run, anddocker compose up -d --buildall tested directly — image builds with no native compile step, container survives a restart with data intact, compose's named volume and.envwiring both work as documented inREADME.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_SECUREand the reverse-proxy notes inREADME.mdare 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 withgit config --global user.name/user.emailif that's wrong, then optionallygit commit --amend --reset-authoron 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.jswires everything up.server.jsis the entry point. - DB: SQLite via the built-in
node:sqlitemodule (notbetter-sqlite3— that failed to compile natively in this environment, andnode:sqlitehas the added benefit of needing zero native build tooling anywhere, including on a NAS/ARM host). Requires Node ≥22.5. Schema insrc/db/schema.sql, applied idempotently on boot bysrc/db/migrate.js. - Sessions: custom SQLite-backed session store
(
src/lib/sqliteSessionStore.js) instead ofconnect-sqlite3, because that package pulls in the nativesqlite3module — same reason as above, avoid native deps entirely. - Auth:
bcryptjs(pure JS) for password hashing,express-sessionfor 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 inREADME.mdas 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 viaexpress.static.public/js/calendarRender.jsis 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, oneeditableflag 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.sqlfor 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.gitignorespecifically to prevent that, but that's a safety net, not a substitute for being careful — e.g. don't rungit add -fon it.GitOServr/GitOservr.pub— the matching public key.GitOServr/known_hosts— the Gitea host's SSH host keys, pinned viassh-keyscan -p 23 git.oservr.comthe 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 deleteknown_hoststo 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.