Operations
Where things are
| What | Where |
|---|---|
| Site | https://calendar.spboucher.ai (Caddy on BHS64 → WireGuard → M4M64c:8660) |
| Node | M4M64c (pinned). Source of truth: M4M64c:~/apps/sp-calendar (git, origin gitsrv:srv/git/sp-calendar.git) |
| Processes (PM2) | sp-calendar-web (next start), sp-calendar-worker (tsx), sp-calendar-backup (cron 03:35) |
| Database | PostgreSQL 17 on M4M64c, database sp_calendar, role spcal_app |
| Secrets | M4M64c:~/apps/sp-calendar/.env (600), ~/.config/sp-calendar/backup-passphrase (600); laptop copy ~/Desktop/Cluster/secrets/sp-calendar/ |
| Backups | M4M64c:~/backups/sp-calendar/*.dump.enc (30 days) |
| Enrolment link | M4M64c:~/data/sp-calendar/setup-url.txt (first passkey, valid 7 days from the first deploy) |
| mld manifest | M1M32:~/dispatch/apps/sp-calendar.json (copy: infra/mld/sp-calendar.json) |
First sign-in (owner)
ssh M4M64c cat ~/data/sp-calendar/setup-url.txtand open the link on the device whose Touch ID / Face ID you want to use (Safari or Chrome). Expired?ssh M4M64c 'cd ~/apps/sp-calendar && node_modules/.bin/tsx scripts/setup-token.ts'.- Create the passkey, then store the eight recovery codes.
- In Settings → Security, add a second passkey from another device (the dashboard reminds you until you do). With iCloud Keychain the passkey syncs to your other Apple devices by itself.
Recovery
- A device is lost but you have a recovery code: sign-in page → "Utiliser un code de récupération", then add a passkey.
- Everything is lost:
ssh M4M64c 'cd ~/apps/sp-calendar && node_modules/.bin/tsx scripts/setup-token.ts --recovery'(30-minute link; enrolling with it removes every other passkey and signs out every session).
Changing the application (remote-first)
ssh M4M64c
cd ~/apps/sp-calendar # edit here
export PATH=/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH
pnpm install --frozen-lockfile # only if dependencies changed
node_modules/.bin/tsx scripts/migrate.ts # applies new db/migrations/*.sql once each (checksummed)
node_modules/.bin/next build && pm2 restart sp-calendar-web sp-calendar-worker
curl -s http://127.0.0.1:8660/api/health
git add -A && git commit -m "…" && git push origin mainMigrations are append-only: never edit an applied file (the runner refuses a changed checksum).
mld deploy sp-calendar redeploys from the gateway's staging and refuses when the node repo is ahead (by design).
.env: MAIL_TRANSPORT=resend + RESEND_API_KEY=… + MAIL_FROM="SP Calendar <calendar@your-verified-domain>",
or MAIL_TRANSPORT=smtp + SMTP_URL=smtps://user:pass@host:465. Then
pm2 restart sp-calendar-web sp-calendar-worker --update-env, and in the admin area: Settings → Notifications →
send a test e-mail, then "Remettre en file" for the notifications parked while the transport was not configured.
The log shows, per message, the transport's message id or the error. MAIL_TRANSPORT=none parks everything.
Health and monitoring
GET /api/health→{ ok, db, worker: { ok, ageSec }, mail: { configured } }(503 when the database is down).mld heal(every 5 minutes on M1M32) restarts missing PM2 processes and repairs the tunnel route.- Dashboard alerts: worker stopped, e-mail not configured / parked / dead, stale external calendar, request whose time is no longer free, single passkey.
- Logs:
pm2 logs sp-calendar-web,pm2 logs sp-calendar-worker. No request body is logged. - Audit trail: Settings → Audit (who did what, when).
Backups and restore
Daily 03:35: pg_dump → AES-256 → ~/backups/sp-calendar/, verified by decrypting and listing the archive.
Run now: pm2 restart sp-calendar-backup (or bash scripts/backup.sh).
Restore test (do it after any change to the backup script, and from time to time):
createdb sp_calendar_restore_test
bash scripts/restore.sh ~/backups/sp-calendar/<latest>.dump.enc postgres://127.0.0.1:5432/sp_calendar_restore_test
psql -d sp_calendar_restore_test -c "select count(*) from bookings"; dropdb sp_calendar_restore_testReal restore: stop both processes, restore into an empty sp_calendar owned by spcal_app, start them.
The passphrase is NOT in the backups: keep ~/Desktop/Cluster/secrets/sp-calendar/backup-passphrase safe.
Off-node copy of the backups is not set up (see PLAN.md).
Moving to another node
The database does not follow mld move. Stop the processes, scripts/backup.sh, create role and database on
the new node, copy .env and the passphrase, scripts/restore.sh, change the pin in the manifest, mld deploy.
Passkeys keep working (they are bound to the domain).
Tests
pnpm typecheck && pnpm lint
pnpm test # Vitest against a real PostgreSQL (database sp_calendar_test, created empty beforehand)
scripts/e2e.sh # Playwright, desktop + mobile, production build, throw-away database sp_calendar_e2e,
# real SMTP transport into a local sink, WebAuthn with Chromium's virtual authenticator
pnpm secrets:scanTroubleshooting
| Symptom | Check |
|---|---|
| Passkey prompt refuses | The site must be opened at exactly APP_URL's host; passkeys are bound to calendar.spboucher.ai. |
| "Lien d'enregistrement invalide" | One use, limited life: issue a new one with scripts/setup-token.ts. |
| Visitor links all invalid | APP_SECRET changed: links derive from it. Restore the previous value. |
| No e-mail leaves | Settings → Notifications: transport state and per-message error. |
| Public page shows no slots | Availability (weekly hours, exceptions), type's notice / horizon / limits, private events, external calendars set to block. |
| Automatic confirmations became requests | An external calendar is stale (Settings → Integrations); policy in Settings → Profile. |