Files
familysync/.planning/ROADMAP.md
T

15 KiB
Raw Blame History

Roadmap: FamilySync

Overview

FamilySync is built in five phases, each delivering an end-to-end user-observable capability. Phase 1 is both the foundation and the highest-risk gate: OIDC auth must work and the CalDAV broker must prove it can read personal Fastmail calendars before any calendar UI is built. Phases 23 complete the calendar. Phase 4 delivers shared lists with live co-edit sync. Phase 5 wires up Web Push notifications. The dependency chain is strict: each phase is a prerequisite for the next, except the lists track (Phase 4) which is independent of the calendar write path.

Phases

Phase Numbering:

  • Integer phases (1, 2, 3): Planned milestone work
  • Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)

Decimal phases appear between their surrounding integers in numeric order.

  • Phase 1: Foundation + Broker Spike - Auth, Docker scaffold, CalDAV broker read path, and personal-calendar ACL spike (go/no-go gate) (completed 2026-06-04)
  • Phase 2: Calendar Display - Read-only unified color-coded calendar (week/month/day/agenda) built on the confirmed broker (completed 2026-06-05)
  • Phase 3: Event Write-Back + PWA Install - Full event CRUD written back to Fastmail, PWA manifest + service worker, guided iOS install flow
  • Phase 4: Shared Lists + Live Sync - Named collaborative lists with item CRUD and real-time SSE co-edit sync
  • Phase 5: Web Push Notifications - VAPID push for event reminders, event changes, and list-change alerts

Phase Details

Phase 1: Foundation + Broker Spike

Goal: The app stack is running, both members can authenticate, and the CalDAV broker can read Fastmail calendars — with a confirmed go/no-go decision on personal-calendar cross-account sharing Mode: mvp Depends on: Nothing (first phase) Requirements: AUTH-01, AUTH-02, AUTH-03, CAL-01, CAL-08 Success Criteria (what must be TRUE):

  1. Both members can reach the app URL, authenticate through Authelia OIDC, and land on the app home page without entering any Fastmail credentials
  2. Sessions persist across browser restarts — neither member is asked to log in again on the next visit
  3. Each member is assigned a stable, distinct display color that does not change between sessions
  4. The broker successfully fetches and caches at least one event from the shared Fastmail calendar via CalDAV PROPFIND/REPORT
  5. The personal-calendar ACL spike produces a documented go/no-go decision: either the broker token sees the wife's personal calendar after Fastmail share+accept, or the fallback strategy (shared-family-only or per-member app password) is chosen and recorded

Verification status (D-14, 2026-06-04): Code + Gate 1 complete. Gate 1 = stack up (/health live), CAL-01 proven live (503 real events cached via REPORT), CAL-08 = GO (per-member app-password model, see CAL-08-DECISION.md). Gate 2 deferred — criteria 1/2/3 (live Authelia OIDC login over Pangolin, session persistence, distinct colors in a real browser) and the SSE-over-Pangolin smoke test require the operator's Authelia + Pangolin/Newt infra; tracked in 01-HUMAN-UAT.md and docs/deployment.md. The live AUTH smoke (incl. iOS) is folded into Phase 3; the SSE smoke is a hard gate before Phase 4. Phases 23 develop behind a documented dev-auth bypass.

Plans: 4 plans Plans:

  • 01-01-PLAN.md — Walking skeleton: monorepo scaffold + Docker/MariaDB + Drizzle schema (push) + /health end-to-end slice + Vitest Wave 0 harness
  • 01-02-PLAN.md — Authelia OIDC slice: stable identity (iss+sub) + auto-assigned member color + /api/me + authenticated PWA shell (AUTH-01/02/03)
  • 01-03-PLAN.md — CalDAV broker slice: AES-256-GCM credential encryption + tsdav broker + ical.js sync (all-day DATE) + ctag poller + /api/events (CAL-01)
  • 01-04-PLAN.md — Integration + gate: wire poller/routes, event-proof landing page, CAL-08 spike + go/no-go doc, live Pangolin deploy + SSE smoke test

Phase 2: Calendar Display

Goal: Both members can see a unified, color-coded calendar aggregating all accessible Fastmail calendars across day, week, month, and agenda views — read-only, no write-back yet Mode: mvp Depends on: Phase 1 Requirements: CAL-02, CAL-03, CAL-07 Success Criteria (what must be TRUE):

  1. Opening the app shows a color-coded calendar where each member's events appear in their assigned color, with shared events distinguishable from personal events
  2. The user can switch between day, week, month, and agenda views and all events render correctly in each view
  3. A recurring event (e.g., weekly meeting) displays all its occurrences correctly in the current view window, including correct behavior across DST boundaries
  4. All-day events (birthdays, holidays) appear as full-day banners on the correct date with no timezone shift

Plans: 5 plansPlans: Wave 1

  • 02-01-PLAN.md — Foundation: schema (hasRrule + calendars.isShared, pushed) + dev-auth bypass + PWA vitest/jsdom harness + ICS fixtures + RED test stubs

Wave 2 (blocked on Wave 1 completion)

  • 02-02-PLAN.md — Backend slice: expandOccurrences() (VTIMEZONE/DST + all-day + EXDATE) + windowed/joined/zod-validated /api/events + shared-calendar checkpoint (CAL-02/CAL-07)
  • 02-03-PLAN.md — Frontend foundation: CSS token layer + colorUtils + calendarConfig (firstDayOfWeek 0→7) + hydrateEvents (Temporal/PlainDate guard) + Zustand store + windowed fetchEvents

Wave 3 (blocked on Wave 2 completion)

  • 02-04-PLAN.md — Vertical slice: CalendarShell mounts Schedule-X, renders real windowed Fastmail events color-coded across all four views (CAL-02/CAL-03)

Wave 4 (blocked on Wave 3 completion)

  • 02-05-PLAN.md — UX completion: read-only EventDetailPopover + ColorLegend + nav/toolbar + skeleton/empty/error states + human visual verification

Gap-closure waves (from 03-REVIEW.md — write path was broken end-to-end; Gate 2 / 03-08 is blocked on these)

  • 03-09-PLAN.md — Route layer: align zod schema to client title/start/end contract (CR-01) + real OIDC iss/sub→users.id resolution on all 5 handlers (CR-06) [wave 1]
  • 03-12-PLAN.md — PWA EventForm: edit-mode population + recurrence preselect (WR-03), zone-consistent dates (WR-05), real focus trap (WR-07); PWA-01/02 install assets verified [wave 1]
  • 03-10-PLAN.md — Worker dispatch: build real VEVENT via buildVeventString + all-day DTEND+1 (CR-02/WR-04), fail closed on bad creds (CR-03), backoff index + randomUUID (WR-01/WR-08) [wave 2, after 03-09]
  • 03-11-PLAN.md — Outbox durability: durable create-before-delete (CR-04), drain concurrency guard (CR-05), fresh-etag-before-PUT (WR-02) [wave 3, after 03-10]

UI hint: yes

Phase 3: Event Write-Back + PWA Install

Goal: Both members can create, edit, and delete events that are written back to the correct Fastmail calendar, and the app is installable to the iPhone and Android home screens with a guided onboarding flow Mode: mvp Depends on: Phase 2 Requirements: CAL-04, CAL-05, CAL-06, CAL-07, PWA-01, PWA-02 Success Criteria (what must be TRUE):

  1. A member can create a timed or all-day event (including recurring events) in the app and see it appear in the native Fastmail app within the next sync cycle
  2. A member can edit an existing event's title, time, or description and the change persists correctly in Fastmail
  3. A member can delete an event and it disappears from all views on the next sync
  4. On Android, the app shows a browser install prompt and installs to the home screen; on iOS, the app shows a guided "Add to Home Screen" walkthrough with annotated screenshots that a non-technical user can follow independently
  5. The installed PWA opens full-screen without browser chrome on both iOS and Android
  6. (Carried from Phase 1 Gate 2, D-14) Live Authelia OIDC login works over the public Pangolin URL — including the iOS standalone-PWA flow: the wife can install to Home Screen and complete login without the redirect breaking out of standalone mode; sessions persist (AUTH-01/02) and members get distinct stable colors (AUTH-03). Verify per docs/deployment.md Gate 2 checklist; this is the first real external deploy (local Newt test rig is sufficient — Unraid prod is optional until go-live).

Plans: 12 plans (8 original + 4 gap-closure from 03-REVIEW.md) Plans: Wave 1

  • 03-01-PLAN.md — Foundation: calendarOutbox table + calendarEvents.objectUrl (pushed), vite-plugin-pwa install + legitimacy gate, sync.ts objectUrl, full Wave 0 RED test scaffold

Wave 2 (blocked on Wave 1)

  • 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
  • 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)

Wave 3 (blocked on Wave 2)

  • 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring
  • 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
  • 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02)

Wave 4 (blocked on Wave 3)

  • 03-06-PLAN.md — Delete + sync feedback: popover Edit/Delete footer + DeleteConfirmationDialog + SyncStateToast polling (D-06/08/09) (CAL-05/06)

Wave 5 (blocked on Wave 4)

  • 03-08-PLAN.md — Gate 2 live verification: real Authelia OIDC over Pangolin + iOS standalone login + end-to-end Fastmail write round-trips (success criterion 6, D-14/D-15)

UI hint: yes

Phase 4: Shared Lists + Live Sync

Goal: Both members can create and manage shared named lists with real-time co-edit sync — edits by one member appear for the other without any manual refresh Mode: mvp Depends on: Phase 1 ⚠️ Entry gate (D-14): The SSE-over-Pangolin smoke test (deferred from Phase 1 Gate 2, issue #1034) MUST pass before building live sync — hold /api/sse/heartbeat open 5+ min through the tunnel without it being cut (see docs/deployment.md). If it FAILS: fix Pangolin idle-timeout/buffering, or plan a reconnect/polling fallback into this phase before proceeding. Do not build the live-sync layer on an unverified transport. Requirements: LIST-01, LIST-02, LIST-03, LIST-04 Success Criteria (what must be TRUE):

  1. Either member can create a named list (e.g., "Groceries") and delete a list they no longer need
  2. Either member can add items to a list, check items off, reorder them by drag-and-drop, and delete individual items
  3. When one member adds or checks off an item, the other member sees the change appear in the list within a few seconds without refreshing — even if they reconnect after a brief network gap

Plans: TBD UI hint: yes

Phase 5: Web Push Notifications

Goal: Both members receive timely Web Push alerts for upcoming events, event changes made by the other member, and list changes — reliably on both iOS and Android Mode: mvp Depends on: Phase 3, Phase 4 Requirements: NOTIF-01, NOTIF-02, NOTIF-03 Success Criteria (what must be TRUE):

  1. A member receives a push notification on their phone approximately 15 minutes before a calendar event starts — delivered to the installed PWA, including on iOS
  2. When the other member adds or changes a calendar event, the first member receives a push notification with the event title and action described in the payload
  3. When the other member modifies a shared list (adds, checks off, or deletes an item), the first member receives a push notification identifying the list and the change
  4. After an extended period of app inactivity, push notifications are still delivered (subscription health-check prevents silent revocation on iOS)

Plans: TBD

Progress

Execution Order: Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is complete. It is serialized here to reduce work-in-progress.

Phase Plans Complete Status Completed
1. Foundation + Broker Spike 4/4 Complete 2026-06-04
2. Calendar Display 5/5 Complete 2026-06-05
3. Event Write-Back + PWA Install 11/12 In Progress
4. Shared Lists + Live Sync 0/? Not started -
5. Web Push Notifications 0/? Not started -

Backlog

Phase 999.1: Treat Fastmail as one calendar provider; framework supports adding more providers (BACKLOG)

Goal: [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern. Requirements: TBD Plans: 11/12 plans executed

Plans:

  • TBD (promote with /gsd-review-backlog when ready)

Phase 999.2: Slick unauthenticated-entry — no calendar/"Sign-in required" flash before Authelia redirect (BACKLOG)

Goal: [Captured for future planning] On a cold unauthenticated load the PWA briefly paints the calendar shell + skeleton, then flashes a "Sign-in required" error, then redirects to Authelia — not slick (violates the "low-friction for the non-technical Apple member" hard constraint). Make unauthenticated entry render a single neutral "Signing you in…" splash and go straight to Authelia, with no app content or error text painted first.

Root cause (diagnosed during Phase 03 Gate 2 live verification, 2026-06-07) — apps/pwa/src/components/CalendarShell.tsx: the component renders optimistically before auth is known. While meQuery (GET /api/me) is pending, isInitialLoading renders the calendar shell + SkeletonCalendar. When meQuery resolves as an opaqueredirect (unauthenticated — fetchMe uses redirect:'manual' in apps/pwa/src/api/client.ts), it errors and in the same tick (1) the early return if (meQuery.isError) return <div role="alert">Sign-in required</div> (~line 187) paints, and (2) a useEffect calls window.location.href='/api/login'. Because the navigation is async, React paints "Sign-in required" for ~one frame before leaving for Authelia. Net: calendar flash → "Sign-in required" flash → Authelia.

Proposed fix: Gate the app render on auth state — (a) don't render CalendarContent/skeleton until meQuery.isSuccess; (b) while unauthenticated and redirecting, render a neutral full-screen "Signing you in…" splash instead of the "Sign-in required" alert; (c) reserve the "Sign-in required" dead-end only for the one-shot-guard fall-through (already bounced through /api/login and still failing). Optionally hoist the auth check above the heavy calendar mount.

Severity: low / cosmetic, but hits every unauthenticated cold load and the wife's first impression. Tags: phase-03, ux-polish, auth. Requirements: TBD Plans: 0 plans

Plans:

  • TBD (promote with /gsd-review-backlog when ready)