# 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 2–3 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.

- [x] **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)
- [x] **Phase 2: Calendar Display** - Read-only unified color-coded calendar (week/month/day/agenda) built on the confirmed broker (completed 2026-06-05)
- [x] **Phase 3: Event Write-Back + PWA Install** - Full event CRUD written back to Fastmail, PWA manifest + service worker, guided iOS install flow (completed 2026-06-07)
- [x] **Phase 4: Shared Lists + Live Sync** - Named collaborative lists with item CRUD and real-time SSE co-edit sync (completed 2026-06-09)
- [x] **Phase 5: Web Push Notifications** - VAPID push for event reminders, event changes, and list-change alerts (completed 2026-06-10; on-device UAT 1/2/5 PASS, T3 dropped as non-gating, T4 Android event-change push deferred to Phase 6 verification — see 05-UAT.md)
- [x] **Phase 6: UX Polish** - All-day visual distinction, event-form date/recurrence behavior, recurring-series edit, and auth-flow smoothing (completed 2026-06-10)

## 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 2–3 develop behind a documented dev-auth bypass.

**Plans**: 4 plans
Plans:

- [x] 01-01-PLAN.md — Walking skeleton: monorepo scaffold + Docker/MariaDB + Drizzle schema (push) + /health end-to-end slice + Vitest Wave 0 harness
- [x] 01-02-PLAN.md — Authelia OIDC slice: stable identity (iss+sub) + auto-assigned member color + /api/me + authenticated PWA shell (AUTH-01/02/03)
- [x] 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)
- [x] 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**

- [x] 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)*

- [x] 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)
- [x] 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)*

- [x] 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)*

- [x] 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)*

- [x] 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]
- [x] 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]
- [x] 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]
- [x] 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**

- [x] 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)*

- [x] 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
- [x] 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)*

- [x] 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
- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
- [x] 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)*

- [x] 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)*

- [x] 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

**Entry gate status (2026-06-08):** CLEARED — SSE-over-Pangolin smoke test PASSED (35 heartbeats over ~6 min, buffering off, no cut). Live sync may be built directly on SSE; polling fallback (D-12) retained as belt-and-suspenders.

**Plans**: 7 plans (6 + 1 gap-closure)
Plans:
**Wave 1**

- [x] 04-01-PLAN.md — Foundation + app shell: deps install (+ legitimacy gate), list tables generate+migrate [BLOCKING], API test harness + Wave-0 RED stubs, react-router + BottomTabBar + empty ListsIndex (D-13/D-16/D-17/D-18)

**Wave 2** *(blocked on Wave 1)*

- [x] 04-02-PLAN.md — TDD: scoped in-memory fan-out (listEmitter) + getAccessibleListIds access scope — the load-bearing D-04 no-leak primitive (LIST-04)
- [x] 04-03-PLAN.md — List CRUD slice: POST/GET/PATCH/DELETE /api/lists with scoped access + auto-share-on-create + ListsIndex/ListCard/CreateListSheet/ListDeleteDialog (LIST-01, D-01/D-02/D-06)

**Wave 3** *(blocked on Wave 2)*

- [x] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09)

**Wave 4** *(blocked on Wave 3)*

- [x] 04-05-PLAN.md — Reorder slice: dnd-kit sortable + generateKeyBetween rank + one-row position PATCH + animate-on-remote (LIST-03, D-13/D-14/D-15)

**Wave 5** *(blocked on Waves 2 + 4)*

- [x] 04-06-PLAN.md — Live-sync slice: scoped /api/sse/lists + fan-out triggers + useListSSE bounded-backoff hook + LiveSyncIndicator + polling fallback (LIST-04, D-04/D-10/D-11/D-12)

**Wave 6** *(gap closure — blocked on Waves 2 + 4)*

- [x] 04-07-PLAN.md — Gap closure: migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03 drag-to-top) + owner-only guard on PATCH isShared (T-04-08/T-04-05) — two TDD features (LIST-03)

**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**: 8 plans (6 waves)
Plans:
**Wave 1**

- [x] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12)

**Wave 2** *(blocked on Wave 1)*

- [x] 05-02-PLAN.md — TDD: pushDispatcher (VAPID send + dual-format payload + 410/404 prune) (D-11)
- [x] 05-03-PLAN.md — TDD: pushCoalescer (per-list/actor debounce, generic copy, self-suppress) (D-01/D-02/D-03)

**Wave 3** *(blocked on Wave 2)*

- [x] 05-04-PLAN.md — Subscribe slice (end-to-end): push subscription API + setVapidDetails, generateSW→injectManifest SW migration (push/notificationclick/denylist), usePushSubscription + PushPermissionPrompt (D-08/D-11/D-14)

**Wave 4** *(blocked on Wave 3)*

- [x] 05-05-PLAN.md — NOTIF-02 list-change slice: listChangeDispatcher + hook coalescer into mutations, reorder-silent (D-01/D-02/D-03)
- [x] 05-06-PLAN.md — TDD: NOTIF-01 reminderScheduler — shared-timed 15-min scan (query-enforced D-05), all-day excl, dedup, empty-set safe (D-05/D-06/D-07)

**Wave 5** *(blocked on Wave 4)*

- [x] 05-07-PLAN.md — TDD: NOTIF-03 eventChangeDispatcher + syncCalendar diff/title/onChanges hook (poller + outbox), meaningful-only, actor-suppressed (D-02/D-03/D-04/D-13)

**Wave 6** *(blocked on Wave 3)*

- [x] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet

**UI hint**: yes

### Phase 6: UX Polish

**Goal**: Smooth the rough edges surfaced during live use — clearer all-day events, saner event-form date/recurrence behavior, recurring-series editing, and auth-flow polish — so the app feels slick for the non-technical Apple member (hard UX constraint).
**Mode:** mvp
**Depends on**: Phase 3 (calendar/event-form polish); Phase 4 for any list-related polish
**Requirements**: none (all v1 REQ-IDs complete in Phases 1–5; this is a polish phase tracked against backlog items 999.2/3/6/7/8/9 and locked decisions D-01..D-13)
**Success Criteria** (what must be TRUE):

  1. All-day events are visually distinct from timed events at a glance
  2. The event form keeps a sane duration when the start moves, all-day edits don't grow the event, and a recurrence can be bounded (repeat-until / count)
  3. A recurring series can be edited as a whole
  4. A session that expires mid-use redirects cleanly to sign-in instead of hanging on a generic error
  5. Unauthenticated cold load shows a neutral "signing you in…" splash — no calendar/"sign-in required" flash before Authelia

**Scope** (promoted from backlog, locked at planning): 999.2 (login flash), 999.3 (session-timeout redirect), 999.6 (all-day visual), 999.7 (form end-tracking + all-day-edit off-by-one), 999.8 (recurrence bound), 999.9 (recurring-series edit). 999.4 (reminders) and 999.5 (provider setup) deferred to milestone 1.1 (D-01/D-02).

**Plans**: 6 plans (2 waves)
Plans:
**Wave 1** *(parallel — exclusive file ownership)*

- [x] 06-01-PLAN.md — TDD: duration-preserving end-tracking math (computeNewTimedEnd/computeNewAllDayEnd) in eventDateTime.ts (D-04)
- [x] 06-02-PLAN.md — TDD: RRULE UNTIL/COUNT serialization + Zod acceptance + FREQ-persistence regression (vevent/outboxWorker/events route) (D-06/D-07)
- [x] 06-03-PLAN.md — TDD: hasRrule on CalendarOccurrence + bounded-expansion lock (expand.ts) (D-06/D-08)
- [x] 06-04-PLAN.md — Spinner/pulse: global @keyframes pulse + remove redundant spin redefinition (D-13)
- [x] 06-05-PLAN.md — Auth gating slice: SessionExpiredError + AuthSplash + global QueryCache/MutationCache error handler; client.ts type mirrors (D-10/D-11, + D-06/D-08 type carriers)

**Wave 2** *(blocked on 06-01/02/03/05)*

- [x] 06-06-PLAN.md — EventForm integration slice: end-tracking wiring + recurrence-bound control + series-edit prompt + all-day pill (D-03/D-04/D-05/D-06/D-07/D-08/D-09/D-12)

**UI hint**: yes

## Progress

**Execution Order:**
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
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 | 12/12 | Complete    | 2026-06-07 |
| 4. Shared Lists + Live Sync | 6/6 | Complete   | 2026-06-09 |
| 5. Web Push Notifications | 8/8 | Complete   | 2026-06-10 |
| 6. UX Polish | 6/6 | Complete   | 2026-06-10 |

## 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:** 3/6 plans executed

Plans:

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

### Phase 999.4: Per-event reminder configuration (VALARM authoring + scheduler honors it) (BACKLOG)

**Goal:** [Captured for future planning] End-to-end per-event reminders — let the user choose *when* (or whether) to be reminded per event, and make the push scheduler honor that choice instead of a hardcoded lead.

**Half A — author the VALARM (event form):** The event create/edit form has no UI to set a reminder ("remind me 10 min / 1 hour / 1 day before", or **no reminder**), so the written `.ics` carries no `VALARM` and no reminder can fire — in native clients or via web push. Add a reminder selector (including an explicit "none"), serialize chosen offsets as `VALARM` (TRIGGER) on write-back, and parse existing `VALARM`s on read so edits preserve them. Feeds the Phase 5 web-push requirement (push needs reminder data to notify about).

**Half B — scheduler honors the provider's value (NEW, surfaced 2026-06-10):** Today `apps/api/src/broker/reminderScheduler.ts` runs a **hardcoded 15-minute** scan for shared timed events (`index.ts:139` "starting in ~15 min"; reminderScheduler header "15-min reminder scan") and never reads the event's actual alarm. So every reminder fires 15 min before regardless of what the event (or the calendar provider) specifies, and an event with **no** alarm still gets a 15-min push. Change the scheduler to read each event's `VALARM` `TRIGGER` (the value written in Half A / set in Fastmail or another native client) and fire at that lead — and fire **nothing** when the event has no alarm. The current fixed 15-min window/dedup logic (catch-up scan, per-uid exactly-once — see quick 260610-hbu) must be generalized to a variable per-event lead.

**Boundary:** preserve the reminder scheduler's resilience guarantees (catch-up on a missed tick, per-uid exactly-once dedup). This makes the lead per-event/variable rather than constant; it is not a rewrite of the scan/dedup design.

**Severity:** medium — feature gap surfaced during Phase 03 Gate 2 testing; Half B surfaced 2026-06-10. Tags: phase-03, phase-05, calendar, write-back, reminders, valarm, push, scheduler, phase-05-dependency.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.5: First-login provider setup — prompt + instructions to add a Fastmail app password (BACKLOG)

**Goal:** [Captured for future planning] On a member's first login there is no onboarding to connect their own calendar provider. Today the broker uses a single seeded Fastmail app password (the operator's), so a second member (e.g. the wife) who logs in sees only what that token reaches — she has no way to attach her **own** Fastmail personal calendar (the D-09 per-member app-password model). Add a first-login flow that detects a member has no `member_credentials` row and prompts them to create + paste a Fastmail app password, with clear step-by-step instructions (where to generate it in Fastmail settings, required scope: Calendars/CalDAV, that one app password covers all of that account's calendars). Store it encrypted (APP_PASSWORD_ENCRYPTION_KEY, existing crypto path), then trigger an initial sync so their personal calendar lane populates.

**Context** (surfaced 2026-06-07, Gate 2 live testing): the wife logged in on her iPhone and added the PWA to her Home Screen, but there is no provider-setup step — so her personal calendar can't be connected. This is the onboarding half of the "each member's personal calendar" v1 requirement.

**Scope to decide when promoted:**

- Detect "no credential yet" state server-side (`GET /api/me` exposes a `needsProviderSetup` flag, or a dedicated endpoint) and gate a setup screen in the PWA.
- App-password entry UI + validation (test the credential with a CalDAV PROPFIND before saving), encrypted storage, and triggering the first sync.
- Non-technical-friendly instructions (the hard UX constraint) — ideally with a direct link to Fastmail's app-password page and a screenshot/walkthrough.
- Decide the model: does every member attach their own personal calendar, or do some members only see the shared family calendar? (Open question from D-16.)
- Security: never log/echo the app password; member-scoped; T-03-19 style scoping.

**Severity:** high for true multi-member use — without it the second member has no personal calendar. Tags: phase-03, onboarding, auth, caldav, per-member-credential, D-09.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.10: Admin Settings / Administration section — manage app passwords + designate the shared calendar via UI (BACKLOG)

**Goal:** [Captured for future planning] Add an in-app **Settings/Administration** section, gated to an administrator role, for configuration that today requires manual backend/DB steps:

- **View/update per-member Fastmail app passwords** (stored encrypted via `APP_PASSWORD_ENCRYPTION_KEY`, existing crypto path) — rotate or re-enter a member's credential and re-trigger sync.
- **Designate which synced calendar is the "shared" calendar** by toggling `calendars.is_shared` from the UI. Today this is a manual DB write: e.g. `UPDATE calendars SET is_shared=1 WHERE id=<row>` — done by hand on 2026-06-10 to mark the "FamilySync" calendar (id 10) shared after the poller synced it (D-16). The admin should pick the shared calendar from a list of synced collections instead of relying on a backend process. (The poller's upsert already leaves `is_shared` untouched, so a UI-set flag persists.)

**Context:** Motivated by the manual D-16 resolution (2026-06-10). **Related:** 999.5 (per-member first-login app-password onboarding) — this is the ongoing admin-managed counterpart; and 999.11 (initial setup wizard) — bootstrap-time vs. ongoing config. Tags: admin, settings, calendar, app-passwords, D-16.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.11: Initial setup wizard — first-run config of env vars, app passwords, DB connection (BACKLOG)

**Goal:** [Captured for future planning] Add a first-run **setup wizard** that walks the administrator through defining all bootstrap configuration instead of hand-editing `.env` / `docker-compose.yml`:

- **App environment variables:** OIDC client id/secret/issuer/redirect URI + external URL, session signing secret (`OIDC_AUTH_SECRET`), `APP_PASSWORD_ENCRYPTION_KEY`, and the **VAPID keypair** (subject + public + private).
- **MariaDB connection:** host/port/user/password/db, with a connectivity test.
- **First Fastmail app password** for the initial member, encrypted on save.

Wizard should **validate inputs before completing** — e.g. VAPID private key decodes to 32 bytes AND pairs with the public key, OIDC discovery resolves, DB connects, app-password reaches CalDAV.

**Context:** Motivated by setup friction observed 2026-06-10 — a VAPID private key truncated on paste into `.env` silently broke push (`setVapidDetails failed — 32 bytes`), and `DB_HOST` / dev overrides must currently be set by hand. A guided + validated wizard would have caught these. **Related:** 999.10 (ongoing admin Settings) and 999.5 (member onboarding). Tags: onboarding, setup, install, env, vapid, mariadb, oidc.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.12: Assistant-driven mobile-browser UI testing (mobile viewport + authed PWA) (BACKLOG)

**Goal:** [Captured for future planning] Give the assistant a way to validate UI/UX changes in a **mobile** browser experience, not just desktop Chromium. Today `playwright-cli` drives a desktop viewport, and the prod stack enforces OIDC (Authelia) so the authed PWA can't be reached headlessly — which is exactly why a string of mobile-only defects this milestone (silent Android notifications, the dead "How to enable" link, iOS/Android session-cookie persistence, install/standalone behaviour) could only be found by the operator on real devices, not by the assistant.

**What this needs (any subset):**

- **Mobile viewport + UA emulation** in the browser harness (e.g. Playwright device descriptors — iPhone/Pixel viewport, touch, mobile user-agent) so layout, tap targets, and responsive behaviour can be checked.
- **An authenticated entry path for automated runs** so the assistant can reach the real PWA past Authelia — e.g. a reusable saved storage-state/cookie, a test-only bypass on a non-prod host, or driving the Authelia login once and reusing the session. (Note: this overlaps the existing `DEV_AUTH_BYPASS`, but that only works on the host-side dev stack, not the prod-mode PWA that has the real service worker. A mobile, authed, SW-enabled target is the gap.)
- Optionally: a documented way to point the harness at the Pangolin HTTPS URL with a persisted session, and/or remote-debug a real device.

**Boundary:** genuinely device-only behaviour (iOS-Safari standalone push, real APNs/FCM delivery, OS notification-channel importance) still needs a human — this item is about everything SHORT of that (responsive layout, tap flows, in-page notification UI states, auth redirects) which a mobile-emulated authed browser *could* cover but currently can't.

**Context:** Surfaced 2026-06-10 during Phase 5 UAT — repeated mobile-only bugs were caught only by the operator because the assistant had no mobile, authenticated browser to test in. **Related:** [[feedback-playwright-verify]] (use playwright-cli over manual verification — this extends it to mobile/authed). Tags: testing, playwright, mobile, pwa, oidc, dx.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.13: Reduce event write-back latency to the calendar provider (outbox drain) (BACKLOG)

**Goal:** [Captured for future planning] Calendar create/edit/delete writes are enqueue-only (`calendarOutbox`, 202 optimistic-accept; D-12/D-05 — no Fastmail call in the route) and flushed to Fastmail by `runOutboxDrain` on a **15-second `setInterval`** (`apps/api/src/broker/outboxWorker.ts`). So a change can take up to ~15s to land in Fastmail (and longer to reflect back in the app, which depends on the separate 5-min poller). Reduce that perceived sync delay so edits feel near-immediate.

**Options to weigh when picking this up:**

- **Event-driven drain (preferred):** trigger an outbox drain immediately after a successful enqueue (in-process signal, or Redis pub/sub which is already available) so the write fires within ~1s instead of waiting for the next tick — keep the 15s `setInterval` as a fallback/retry sweep. Must preserve the existing per-row etag/412 handling and the rapid-successive-edit ordering (see outboxWorker comments ~L312 — each edit carries its enqueue-time etag).
- **Shorter interval:** simplest, but more idle DB polling; a floor (e.g. 3–5s) trades latency for load.
- **Faster read-back too:** the user also sees latency from the 5-min poller reflecting the change back. Consider invalidating/short-poll after a local write, or optimistic UI already covering it — confirm whether the perceived delay is the write (15s) or the read-back (5min).

**Boundary:** the optimistic 202 + outbox durability design (create-before-delete, drain concurrency guard, fresh-etag-before-PUT) must be preserved — this is a latency tune, not a rewrite of the write path.

**Context:** Surfaced 2026-06-10. Tags: calendar, write-back, outbox, latency, redis, performance.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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

### Phase 999.14: Gitea CI — full regression on PR to main + build/publish Docker image (BACKLOG)

**Goal:** [Captured for future planning] The repo is committed against a self-hosted Gitea instance with a registered Actions runner, but there is no CI yet (no `.gitea/workflows/` or `.github/workflows/`). Two things should run automatically: (1) **full regression** on every PR targeting `main` — gating the merge; (2) **build the app's Docker image and publish it** to the Gitea container registry.

**Options / decisions to make when picking this up:**

- **Test scope:** "full regression" = lint + typecheck + unit + the API integration tests. Integration tests need a real MariaDB (see [[api-integration-test-db]]) — the workflow must spin up a MariaDB service container, bind it, and set `DB_HOST=127.0.0.1` + `.env` creds. The PWA build/test also runs.
- **Monorepo:** pnpm workspace (`apps/api`, `apps/pwa`, shared). Cache the pnpm store.
- **Docker images:** only `apps/api/Dockerfile` exists today — there is no PWA Dockerfile yet. Decide one image (API) vs. also building/serving the PWA. Tag scheme + when to publish (only on merge to `main`? on tags? per-PR?).
- **Registry auth:** push to the Gitea registry using the runner's Gitea-provided token or a dedicated package-write token.
- Gitea Actions are GitHub-Actions-compatible syntax but run on the self-hosted runner — confirm runner labels and available images, and that Actions is enabled, before authoring.

**Likely shape:** a `.gitea/workflows/ci.yml` — `on: pull_request` (to `main`) → install (pnpm), lint, typecheck, unit, API integration vs. a `mariadb` service container, PWA build; `on: push` to `main`/tag → `docker build apps/api/Dockerfile`, login, push tagged image.

**Context:** Promoted from STATE.md pending todo (`.planning/todos/pending/2026-06-10-gitea-ci-regression-and-docker-publish.md`), surfaced 2026-06-10. Tags: tooling, ci, gitea, docker, mariadb, monorepo.
**Requirements:** TBD
**Plans:** 0 plans

Plans:

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