- 01-04-SUMMARY.md: code-complete; Task 1 committed (48f90ce); Tasks 2+3 are live-gate checkpoints with exact human-action steps
- CAL-08-DECISION.md: template for human to fill in after running apps/api/src/broker/spike.ts against real Fastmail credentials
12 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation-broker-spike | 04 | integration |
|
|
|
|
|
|
|
~3min (code tasks only; live verification pending) | 2026-06-04 |
Phase 01 Plan 04: Integration + Gate Slice — Summary
Full Hono bootstrap wired (broker + all routes), SSE heartbeat endpoint for Pangolin smoke test, CAL-08 spike script, and EventProof landing page component. Code-complete. Live verification (CAL-08 spike + Authelia login + Pangolin SSE smoke) pending human action.
Performance
- Duration: ~3 min (code tasks)
- Started: 2026-06-04T15:13:15Z
- Completed: 2026-06-04 (code-complete)
- Tasks: 1 of 3 complete (Tasks 2 + 3 are live-gate checkpoints)
- Files modified/created: 8
Accomplishments
Task 1 (implemented and committed):
apps/api/src/routes/sse.ts:sseRouterwithGET /heartbeatusingstreamSSE— emitsheartbeatevents every 10s untilstream.aborted; mounted under/api/ssebehindoidcAuthMiddleware(T-04-01)apps/api/src/index.ts: final bootstrap —/callback→/health(pre-guard) →app.use('/api/*', oidcAuthMiddleware())→/api/me→/api/events→/api/sse→startBrokerPoller()→serveStaticapps/api/src/broker/spike.ts: CAL-08 spike script — readsFASTMAIL_EMAIL+FASTMAIL_APP_PASSWORDfrom env, callscreateFastmailClient→fetchCalendars(), prints each calendar's URL, displayName, ctag, syncTokenapps/pwa/src/api/client.ts:fetchEvents()withCalendarEventandEventsResponsetypesapps/pwa/src/components/EventProof.tsx: React Query['events']→fetchEvents()→ renders first event's title (ical.js-parsed SUMMARY) + date, or "No cached events yet" empty stateapps/pwa/src/App.tsx: rendersMemberBadge(member name + color) andEventProoftogether on the landing page- All 24 API unit tests green;
tsc --noEmitclean in bothapps/apiandapps/pwa
Task Commits
- Task 1:
48f90ce—feat(01-04): wire broker + routes into bootstrap, add SSE endpoint, EventProof
Files Created/Modified
apps/api/src/routes/sse.ts—sseRouter,GET /heartbeat(streamSSE, 10s heartbeat, T-04-01)apps/api/src/broker/spike.ts— CAL-08 spike: env creds → createFastmailClient → fetchCalendars → print URLsapps/api/src/index.ts— final bootstrap wiring: all 3 API routes + poller startapps/pwa/src/components/EventProof.tsx— broker-proof component, React Query, ical.js SUMMARY extractionapps/pwa/src/api/client.ts—fetchEvents()added with typed shapesapps/pwa/src/App.tsx— MemberBadge + EventProof on landing pageapps/pwa/package.json— ical.js@2.2.1 added.planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md— decision template (fill in after spike)
Live Verification Pending (Tasks 2 + 3)
These are checkpoint:human-action tasks that require real infrastructure:
Task 2: CAL-08 Spike — Confirm app password reads shared + personal calendars
What to do:
-
Generate a Fastmail app password for Lucas's account:
- Fastmail Settings → Privacy & Security → App Passwords → scope: "Mail, Contacts & Calendars"
-
Run the spike:
cd apps/api FASTMAIL_EMAIL=lucas@fastmail.com \ FASTMAIL_APP_PASSWORD=<app-password> \ pnpm exec tsx src/broker/spike.ts -
Confirm in output: (a) shared family calendar collection URL appears; (b) Lucas's personal calendar URL appears. Record both.
-
To insert the credential into the DB for the broker to use (one-off node script):
# First ensure APP_PASSWORD_ENCRYPTION_KEY is set in .env cd apps/api APP_PASSWORD_ENCRYPTION_KEY=<64-char-hex> node -e " import('./src/broker/crypto.js').then(({ encryptPassword }) => { console.log(encryptPassword('<app-password>')) }) " # Then insert the encrypted value into member_credentials for Lucas's user row -
Start the stack and verify at least one event lands in
calendar_events:docker compose exec mariadb mariadb -ufamilysync -p<pw> familysync \ -e "SELECT id, uid, all_day FROM calendar_events LIMIT 5;" -
Fill in
.planning/phases/01-foundation-broker-spike/CAL-08-DECISION.mdwith:Decision: GOorDecision: NO-GO + fallback- Discovered calendar URLs
- Which ctag/syncToken field Fastmail actually returned
-
Reload the app and confirm landing page shows one real cached event.
Resume signal: Type "approved" once CAL-08-DECISION.md records GO (or NO-GO + fallback) and at least one event row is cached.
Task 3: Live Deployment — Authelia login + Pangolin SSE smoke test
What to do:
-
Register FamilySync OIDC client in Authelia (see 01-02-SUMMARY.md for full YAML):
authelia crypto hash --sha512 <your-plain-client-secret>Add the client block to Authelia's
configuration.ymland reload. -
Set env vars in
.env:OIDC_AUTH_SECRET=<openssl rand -base64 32> OIDC_ISSUER=https://auth.<domain> OIDC_CLIENT_ID=familysync OIDC_CLIENT_SECRET=<plain-secret> OIDC_REDIRECT_URI=https://familysync.<domain>/callback OIDC_AUTH_EXTERNAL_URL=https://familysync.<domain> -
Expose FamilySync through Pangolin under the SAME parent domain as Authelia (same-site cookie requirement — Pitfall 1).
docker compose up -d -
AUTH-01: From external network → open https://familysync. → confirm Authelia redirect → log in → see name + color + cached event.
-
AUTH-02: Close browser, reopen URL → confirm no re-login.
-
Repeat for second member (wife) → confirm distinct color (AUTH-03).
-
SSE smoke test (D-08) — run from external network with a valid session cookie:
curl -N -b "session=<cookie-value>" https://familysync.<domain>/api/sse/heartbeatKeep open 5+ minutes. Record: PASS (events keep arriving) or FAIL (stream cut by proxy).
-
Record SSE smoke result in SUMMARY (update this file) for Phase 4 transport decision.
Resume signal: Type "approved" once both members log in over the public URL, sessions persist, and SSE smoke result (PASS/FAIL) is recorded.
SSE Smoke Test Result
Result: PENDING — to be filled in after Task 3.
If PASS: SSE confirmed for Phase 4 real-time list sync. If FAIL: investigate Pangolin idle-timeout config; note for Phase 4.
Deviations from Plan
[Rule 2 - Missing dependency] ical.js added to PWA for EventProof summary parsing
- Found during: Task 1 (EventProof.tsx implementation)
- Issue: EventProof.tsx parses rawVevent strings using ical.js to extract human-readable SUMMARY. ical.js was only in apps/api; EventProof runs in the browser.
- Fix: Added ical.js@2.2.1 to apps/pwa/package.json. Same package, same version, already approved in the legitimacy audit.
- Files modified: apps/pwa/package.json, pnpm-lock.yaml
- Commit:
48f90ce
Known Stubs
CAL-08-DECISION.md is committed as a template with Decision: [PENDING]. The actual go/no-go decision must be filled in by the human after running the spike against real Fastmail credentials. This is the expected state for a code-complete + pending-live-verification plan.
Threat Surface Scan
All surfaces within Plan 04 threat model:
- T-04-01 (SSE auth): sseRouter mounted under
/api/ssebehindoidcAuthMiddleware— confirmed - T-04-02 (SSE payload): heartbeat carries only
{ ts, id }— no user data or secrets - T-04-03 (same-site cookies): operator must expose FamilySync under same parent domain as Authelia — documented in Task 3 steps
- T-04-04 (spike credentials): spike reads password from env, prints only calendar URLs, never echoes the password — confirmed in spike.ts
- T-04-05 (client_secret): plain secret in OIDC_CLIENT_SECRET env; Authelia YAML holds hash — documented in Task 3 steps
- T-04-SC (tsx dev runner): spike.ts not imported by any production module; dev-only
Self-Check: PASSED
apps/api/src/routes/sse.tsexists and exports sseRouter: FOUNDapps/api/src/broker/spike.tsexists: FOUNDapps/api/src/index.tscontainsstartBrokerPoller(): FOUNDapps/api/src/index.tscontains'/api/events'and'/api/sse': FOUNDapps/pwa/src/components/EventProof.tsxexists: FOUNDapps/pwa/src/api/client.tscontainsfetchEvents: FOUND- Commit
48f90ce: FOUND tsc --noEmitclean (apps/api): PASSEDtsc --noEmitclean (apps/pwa): PASSEDpnpm vitest run24/24 tests green: PASSED
Phase: 01-foundation-broker-spike Completed (code): 2026-06-04 — Live verification pending