# Roadmap: FamilySync ## Milestones - ✅ **v1.0 MVP** — Phases 1–6 (shipped 2026-06-10) — see [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) - 🚧 **v1.1 Operability & Polish** — Phases 7–12 (planning) — mobile test harness, Gitea CI (runs the harness), faster write-back, in-app admin, per-event reminders, guided setup ## Phases
✅ v1.0 MVP (Phases 1–6) — SHIPPED 2026-06-10 - [x] Phase 1: Foundation + Broker Spike (4/4 plans) — completed 2026-06-04 - [x] Phase 2: Calendar Display (5/5 plans) — completed 2026-06-05 - [x] Phase 3: Event Write-Back + PWA Install (12/12 plans) — completed 2026-06-07 - [x] Phase 4: Shared Lists + Live Sync (7/7 plans) — completed 2026-06-09 - [x] Phase 5: Web Push Notifications (8/8 plans) — completed 2026-06-10 - [x] Phase 6: UX Polish (6/6 plans) — completed 2026-06-10 Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md).
### 🚧 v1.1 Operability & Polish (Phases 7–12) Make FamilySync configurable, administrable, and maintainable for real multi-member use — without hand-editing env files or the database. The new critical path runs **mobile test harness → Gitea CI** (CI consumes the harness specs for UI regression), and the **admin role → reminders / setup wizard** chain (a single `/api/admin` + `/api/setup` route surface carrying the v1.1 DB migration). Faster write-back is a fully independent track. - [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11) - [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11) - [ ] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee - [ ] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar - [ ] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice - [ ] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface ## Phase Details > v1.0 phase detail (Phases 1–6) is archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md). ### Phase 7: Mobile Test Harness **Goal**: The assistant can drive the PWA in a mobile-emulated, authenticated browser context against the host-side dev stack, so mobile-only layout and flow defects can be caught automatically instead of only by the operator on real devices. This harness is also the artifact Phase 8 (CI) runs for UI regression. **Mode:** standard **Depends on**: Nothing (fully independent; goes first. One new dev dependency `@playwright/test` in `apps/pwa`; no backend changes). **Requirements**: TEST-01, TEST-02 **Success Criteria** (what must be TRUE): 1. An automated run can load the PWA in a mobile-emulated viewport (device profile + mobile UA + touch) and assert on responsive layout / tap targets. 2. The automated run reaches the authenticated PWA via the existing `DEV_AUTH_BYPASS` on the host-side dev stack — no manual login and no Authelia/OIDC mocking. 3. The harness runs repeatably day-over-day without re-capturing any session state (no stale storage-state failures). 4. The harness specs are structured so they can run headlessly in CI (Phase 8) against a dev stack the runner brings up — no dependence on a developer's already-running host stack. **Pitfalls this phase owns** (from PITFALLS.md): - **No stale storage-state** (Pitfall 14): use `DEV_AUTH_BYPASS=true` for the automated harness rather than a checked-in storage-state.json with an expiring session cookie; decide the auth strategy before the first test. - **Service worker block** (Pitfall 15): set `serviceWorkers: 'block'` (or explicitly unregister) in the context so a previous run's SW does not intercept requests / return stale cached responses; verify no SW-sourced responses in the trace. - Hard constraints: targets the dev build via `DEV_AUTH_BYPASS` (DEV_AUTH_BYPASS user 1 has no CalDAV credential/calendars — verify layout/flows, not live event-create); real prod-service-worker / iOS-Safari-standalone mobile testing stays a human/device gate (out of scope). **Plans**: 4 plans (3 waves)Plans: **Wave 1** - [x] 07-01-PLAN.md — Harness foundation: @playwright/test + WebKit/Chromium browsers, playwright.config.ts (iPhone/WebKit + Pixel/Chromium matrix, serviceWorkers block, env baseURL, vite webServer), vitest exclude, scripts (Wave 1) **Wave 2** *(blocked on Wave 1 completion)* - [x] 07-02-PLAN.md — global-setup.ts: /health readiness poll + deterministic mysql2 reset-and-seed (calendar id 10 INSERT IGNORE guard, list + items) + e2e README/guardrails (Wave 2) **Wave 3** *(blocked on Wave 2 completion)* - [x] 07-03-PLAN.md — layout.spec.ts: tap targets >=44px, no overflow, in-viewport, accessible names (UI-SPEC Rules 1-4) + harness self-validation injected-defect proofs (Wave 3) - [x] 07-04-PLAN.md — calendar.spec.ts + lists.spec.ts: populated/empty/error states (Rules 4/5) + DEV_AUTH_BYPASS auth-reached + no-SW-controller precondition (Wave 3) **UI hint**: yes ### Phase 8: Gitea CI **Goal**: Every PR to `main` runs a full regression that gates the merge — lint, typecheck, unit, API-integration against a MariaDB service container, **and the Phase 7 mobile Playwright harness as a UI-regression step against a CI-hosted dev stack** — and a merge to `main` builds and publishes the API Docker image, all on the existing self-hosted Gitea Actions runner. **Mode:** standard **Depends on**: Phase 7 (the PR regression runs the Phase 7 mobile harness specs as its UI-regression step; without the harness there is nothing to run). No other code dependencies. Start with a runner-probe step. **Requirements**: CI-01, CI-02 **Success Criteria** (what must be TRUE): 1. Opening or updating a PR targeting `main` triggers a workflow that runs lint, typecheck (both apps), unit tests, and API integration tests against a MariaDB service container — and a failing run blocks the merge. 2. The API integration tests connect to the service-container MariaDB (DB_HOST=127.0.0.1, service creds) and pass reliably on a cold first run, not only on re-run. 3. The same PR workflow brings up the dev stack inside the runner — the API dev server, the PWA dev server, and the MariaDB service container, with `DEV_AUTH_BYPASS=true` — and runs the Phase 7 mobile Playwright harness specs headlessly against that authed PWA; a harness failure blocks the merge. 4. The harness step waits for both the API and PWA dev servers to be ready (readiness probe / poll) before launching Playwright, so it does not flake on startup races. 5. On merge to `main`, the API Docker image is built and pushed to the Gitea container registry under a sensible tag. 6. Registry credentials never appear in plaintext in the CI logs. **Pitfalls this phase owns** (from PITFALLS.md): - **Runner-probe first** (Pitfall 12): the first workflow only probes `node --version` / `pnpm --version` / Docker access on the `self-hosted` runner before any test or build steps are designed; pin Node 22 explicitly, do not assume `actions/setup-node` works as on GitHub. - **MariaDB readiness wait** (Pitfall 11): add an explicit readiness loop (e.g. `healthcheck.sh --connect --innodb_initialized`, NOT `mysqladmin ping` which is removed in MariaDB 11) before any `drizzle-kit migrate` / integration test step; healthy ≠ accepting connections. - **Dev-stack readiness races (NEW for the harness step):** running the PWA and API dev servers *inside* CI adds startup/readiness races on top of the MariaDB-11 readiness race. The harness step must wait for **both** the API and PWA dev servers to be accepting connections (poll their URLs / health endpoints) before Playwright launches — do not race the browser against a not-yet-listening server. Run with `DEV_AUTH_BYPASS=true` so the harness reaches the authed PWA exactly as in Phase 7. - **--password-stdin** (Pitfall 13): `docker login` via `--password-stdin` with the token piped from a registered Gitea secret (PAT with `write:package`); never `-p $TOKEN` on the command line. - Hard constraints: API integration tests need a real MariaDB and live in `apps/api/tests/` (never `src/`); cache the pnpm store; Drizzle generate+migrate to set up the CI DB schema; the harness step reuses the Phase 7 specs unchanged (CI owns only the stack bring-up + readiness wait, not the spec content). **Plans**: 4 plans (4 waves)Plans: **Wave 1** - [x] 08-01-PLAN.md — Runner probe + operator runner/PAT registration (W0; answers the Docker-vs-host fork) **Wave 2** *(blocked on Wave 1 completion)* - [ ] 08-02-PLAN.md — ci.yml: fast-checks (lint/typecheck/PWA unit) + API job (MariaDB service + migrate + DB-backed tests) **Wave 3** *(blocked on Wave 2 completion)* - [ ] 08-03-PLAN.md — ci.yml: harness job (dev-stack bring-up + readiness waits + Phase 7 Playwright specs, both profiles) **Wave 4** *(blocked on Wave 3 completion)* - [ ] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1- via --password-stdin) **UI hint**: yes ### Phase 9: Faster Write-Back **Goal**: A created, edited, or deleted event reaches Fastmail within ~1-2 seconds (event-driven outbox drain) instead of waiting up to ~15s for the next interval tick — with every existing durability guarantee intact. **Mode:** standard **Depends on**: Nothing (fully independent; the only new artifact is a zero-dependency in-process EventEmitter, `lib/outboxTrigger.ts`). Can run in parallel with any other v1.1 track. **Requirements**: CAL-15 **Success Criteria** (what must be TRUE): 1. After creating/editing/deleting an event, the change lands in Fastmail in ~1-2s in the common case (drain is signalled on enqueue, not waited-for on the interval) — observable as the change appearing in the Fastmail native app well before the old ~15s window. 2. The route handler still returns an optimistic 202 immediately and never makes a CalDAV call inline — the event-driven signal is fire-and-forget. 3. Edit-as-move still writes the new event before deleting the old one (create-before-delete ordering preserved); no event is ever lost when a move drains under rapid enqueues. 4. No duplicate CalDAV PUTs occur for the same outbox row when the signal and the 15s fallback interval overlap (exactly-once per uid preserved). 5. The 15s `setInterval` fallback still runs and recovers any rows missed by the signal path (startup catch-up, transient errors). **Pitfalls this phase owns** (from PITFALLS.md): - **No double-drain** (Pitfall 5): the trigger must set a `drainRequested` flag funnelled through the single setInterval-controlled path / the existing `isDraining` guard — never call `runOutboxDrain()` directly from the signal in a way that bypasses the guard or escapes the error-caught wrapper. - **Create-before-delete under concurrent enqueues** (Pitfall 6): enqueue CREATE before DELETE; do not fire the signal between the two inserts of a move (publish after both inserts / after the transaction commits). - Hard constraints: `setInterval` only (no node-cron); single-process by design — **no Redis** for the drain (Redis stays for list SSE); all outbox guarantees (fresh-etag-before-PUT, 412 conflict flow, per-uid exactly-once) unchanged. **Plans**: TBD ### Phase 10: Admin Role & Settings **Goal**: An admin can manage household configuration that previously required manual DB writes — rotating a member's Fastmail app password and designating the shared family calendar — from a role-gated in-app Settings section, on top of the v1.1 DB foundation this phase introduces. **Mode:** standard **Depends on**: Nothing required upstream; this phase **carries the v1.1 DB migration** (users.is_admin, calendar_events.reminder_lead_minutes, app_config table) that Phases 11 and 12 build on. It is the head of the admin chain (10 → 11, 10 → 12). **Requirements**: ADMIN-01, ADMIN-02, ADMIN-03 **Success Criteria** (what must be TRUE): 1. An admin sees an Admin section in Settings and can list household members with their credential status; a non-admin member never sees it and cannot invoke any `/api/admin/*` route (gets 403). 2. An admin can enter or rotate a member's Fastmail app password; it is validated against CalDAV (PROPFIND) before saving and stored encrypted — and the password is never displayed, echoed in a response, or logged. 3. An admin can pick which synced calendar is the shared family calendar from a list, and the `calendars.is_shared` flag updates accordingly (replacing the manual `UPDATE calendars SET is_shared=1` step). 4. The role check is role-agnostic and member-count-agnostic: it gates on `users.is_admin`, so more admins can be added later without reworking the guard. 5. The DB migration (is_admin, reminder_lead_minutes, app_config) is applied via generate+migrate and is in place for downstream phases (reminder_lead_minutes for Phase 11, app_config.setup_complete for Phase 12). **Pitfalls this phase owns** (from PITFALLS.md): - **Admin role check inside the sub-router** (Pitfall 9): apply `requireAdmin` with `.use('*', ...)` inside `adminRouter`, not only at the parent mount; integration test must assert 403 for a non-admin authenticated user. - **App password never logged/echoed** (Pitfall 7): custom zod-validator `hook` returns a generic 400 (no Zod `received`/`value` field); no `console.log` of request bodies in `routes/admin*`. - Hard constraints: Drizzle **generate+migrate, never push** (false destructive diff on populated MariaDB); reuse `broker/crypto.ts` `encryptPassword` (no changes to crypto); `/api/admin/credentials` and `/api/admin/calendars/:id/shared` are the single shared surface — do NOT duplicate them into `/api/setup/*` in Phase 12. **Plans**: TBD **UI hint**: yes ### Phase 11: Per-Event Reminders **Goal**: A user can choose a reminder lead time per event (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d, default None), serialized as a VALARM on the event, and the push scheduler fires at that exact lead — firing nothing when there is no alarm and never stripping reminders set in other clients. **Mode:** standard **Depends on**: Phase 10 (the `calendar_events.reminder_lead_minutes` column from the v1.1 migration is the scheduler's ground truth). Independent of Phases 7/8/9/12. **Requirements**: CAL-13, CAL-14, NOTIF-04, NOTIF-05, NOTIF-06 **Success Criteria** (what must be TRUE): 1. When creating or editing a timed event, the user can pick a reminder lead from the preset list (None default); the choice round-trips to Fastmail as a VALARM and is visible/honored on re-open. 2. Editing an event that already has a reminder set in another client (Fastmail / Apple Calendar) preserves that VALARM — it is never silently dropped on round-trip. 3. A reminder push fires at the event's chosen lead time (e.g. T-30 for a 30-minute lead), not a hardcoded 15-minute lead. 4. An event with no reminder set produces no reminder push (no default 15-minute fire). 5. An all-day event's reminder fires at a sensible local time (9 AM on the alert day), not midnight; the reminder selector is disabled/hidden for all-day events in the UI; and reminder delivery stays exactly-once across catch-up scans and rescheduled events. **Pitfalls this phase owns** (from PITFALLS.md): - **Preserve-on-edit** (Pitfall 1): the update path extracts and preserves existing VALARM sub-components from `rawVevent` (mirroring the WR-01 RRULE-preserve pattern) — never rebuild-from-scratch and silently strip; `outboxPayloadSchema` distinguishes "no change" from explicit "no reminder". - **No TRIGGER VALUE=TEXT** (Pitfall 2): build the trigger with `ICAL.Duration.fromSeconds(-n*60)`, not a bare string; unit-test that the ICS emits a DURATION trigger with no `VALUE=TEXT`. - **All-day 9AM semantics** (Pitfall 3): guard `buildVeventString` (`if (!allDay && reminderMinutes > 0)`), disable the selector when allDay, keep the scheduler's all-day handling at 9 AM local. - **uid:dtstartMs dedup** (Pitfall 4): change the scheduler dedup key from bare `uid` to compound `uid:dtstartMs` and widen the scan to a variable per-event window so long leads fire and rescheduled events re-fire; keep `eventFieldsSchema` and `outboxPayloadSchema` in sync (IN-03). - Hard constraints: `setInterval` only; scheduler reads `reminder_lead_minutes` from the DB (ground truth), not the outbox payload; drop the `isShared`-only reminder restriction (a user who set an alarm wants it regardless of calendar). **Plans**: TBD **UI hint**: yes ### Phase 12: Initial Setup Wizard **Goal**: On first run (no admin/credentials configured), the operator is guided through a validated, step-by-step wizard to bootstrap the app — env presence, generated secrets to copy, DB/OIDC/VAPID/app-password validation — instead of hand-editing `.env` / `docker-compose.yml`; once complete, the setup endpoints lock. **Mode:** standard **Depends on**: Phase 10 (reuses the admin role + `/api/admin/credentials` and `/api/admin/calendars/:id/shared` routes; the wizard is the second frontend consumer of that surface, and `app_config` from the Phase 10 migration holds `setup_complete`). Goes last. Independent of Phases 7/8/9/11. **Requirements**: SETUP-01, SETUP-02, SETUP-03, SETUP-04 **Success Criteria** (what must be TRUE): 1. On a fresh install with nothing configured, the operator reaches a setup wizard (via `GET /api/setup/status` mounted before the OIDC guard) and walks through bootstrap steps instead of editing files by hand. 2. Each input is validated before the step can complete: DB connects, VAPID private key decodes to exactly 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND). 3. Generated secrets (session secret, encryption key, VAPID keypair) are displayed for the operator to copy into env; they are never written to the DB or returned in a way that persists, and `APP_PASSWORD_ENCRYPTION_KEY`/`VAPID_PRIVATE_KEY` never enter the DB at all. 4. After completion, the wizard-completing user is promoted to admin (`is_admin`), `app_config.setup_complete` is set, and any further call to a setup endpoint returns 423 Locked. 5. The 423 guard is enforced on every invocation (checked against member-credentials + VAPID env present), not only at startup. **Pitfalls this phase owns** (from PITFALLS.md): - **Guard on every invocation** (Pitfall 8): the "already set up" guard returns 423 from all setup routes once configured — implement and test the guard before the happy path; a second POST after completion must return 423, not 200. - **Secrets stay in env, never in DB** (Pitfalls 8 & 10): the wizard validates secrets by performing a test operation (test encrypt/decrypt, structural VAPID check), never by accepting/storing the key value; no DB column for `vapid_private_key` or `app_password_encryption_key`; never log/echo the app password. - Hard constraints: `GET /api/setup/status` mounts **before** the OIDC guard (like `/health`); do NOT create `/api/setup/credentials` — reuse the Phase 10 admin routes; Drizzle generate+migrate (any `app_config` seeding via migration). **Plans**: TBD **UI hint**: yes ## Progress | Phase | Milestone | Plans Complete | Status | Completed | | ----- | --------- | -------------- | -------- | ---------- | | 1. Foundation + Broker Spike | v1.0 | 4/4 | Complete | 2026-06-04 | | 2. Calendar Display | v1.0 | 5/5 | Complete | 2026-06-05 | | 3. Event Write-Back + PWA Install| v1.0 | 12/12 | Complete | 2026-06-07 | | 4. Shared Lists + Live Sync | v1.0 | 7/7 | Complete | 2026-06-09 | | 5. Web Push Notifications | v1.0 | 8/8 | Complete | 2026-06-10 | | 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 | | 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 | | 8. Gitea CI | v1.1 | 1/4 | In Progress| | | 9. Faster Write-Back | v1.1 | 0/? | Not started | - | | 10. Admin Role & Settings | v1.1 | 0/? | Not started | - | | 11. Per-Event Reminders | v1.1 | 0/? | Not started | - | | 12. Initial Setup Wizard | v1.1 | 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:** 1/4 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 > **Promoted into v1.1 Phase 11 (Per-Event Reminders) — CAL-13/CAL-14/NOTIF-04/05/06.** Backlog entry retained for history. 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. > **Note:** v1.1 covers the *admin-managed* counterpart (ADMIN-01, Phase 10) — an admin can set any member's app password. *Self-service* member onboarding (member adds their own) stays deferred here. **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=` — 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. > **Promoted into v1.1 Phase 10 (Admin Role & Settings) — ADMIN-01/ADMIN-02/ADMIN-03.** Backlog entry retained for history. **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. > **Promoted into v1.1 Phase 12 (Initial Setup Wizard) — SETUP-01/02/03/04.** Backlog entry retained for history. **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. > **Promoted into v1.1 Phase 7 (Mobile Test Harness) — TEST-01/TEST-02.** v1.1 scopes the `DEV_AUTH_BYPASS` dev-build path; the prod-SW authed-mobile target stays deferred. Backlog entry retained for history. **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. > **Promoted into v1.1 Phase 9 (Faster Write-Back) — CAL-15.** In-process EventEmitter chosen (not Redis); the drain is single-process by design. Backlog entry retained for history. **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. > **Promoted into v1.1 Phase 8 (Gitea CI) — CI-01/CI-02.** v1.1 also extends CI-01 to run the Phase 7 mobile harness as a UI-regression step (CI brings up the dev stack in the runner). Backlog entry retained for history. **Requirements:** TBD **Plans:** 0 plans Plans: - [ ] TBD (promote with /gsd-review-backlog when ready) ### Phase 999.15: Desktop e2e coverage — add a Desktop Playwright profile + desktop-safe specs (BACKLOG) **Goal:** [Captured for future planning] The Playwright harness (`apps/pwa/playwright.config.ts`) defines only **mobile** device profiles — `iphone` (iPhone 14 / WebKit) and `pixel` (Pixel 7 / Chromium), both with touch and a mobile viewport. The Phase 8 CI regression gate runs `pnpm test:e2e`, so it currently validates the **mobile experience only**. Add desktop coverage so the regression gate exercises the desktop layout/flows as well. **Options / decisions to make when picking this up:** - **Add a Desktop profile:** a new `desktop` project in `playwright.config.ts` (e.g. `devices['Desktop Chrome']`, no `hasTouch`, wide viewport). Optionally a Desktop WebKit/Safari profile too — but the family's Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser. - **Spec-compat pass (the real work):** the existing e2e specs were authored for mobile — they may assume touch gestures, a mobile nav/drawer, or mobile-only layout. Each spec needs review/adjustment so it passes (or is appropriately skipped) on a no-touch, wide-viewport desktop. This is harness/spec work, not CI plumbing. - **Gating choice:** decide whether desktop runs block the merge immediately, or run advisory (non-blocking) until the specs are confirmed desktop-safe. **Boundary:** Phase 8 deliberately reused the Phase 7 harness **unchanged** (CI owns only stack bring-up + readiness waits, not spec content), which is why this was deferred. Once a Desktop project is added to the config, Phase 8 CI picks it up automatically via `pnpm test:e2e` — no CI changes needed beyond whatever runtime/wait the desktop profile requires. **Context:** Deferred from Phase 8 (Gitea CI) planning, 2026-06-11 — user wants both mobile and desktop validated, but desktop needs a config addition + spec review that is out of Phase 8's CI-plumbing scope. Tags: testing, playwright, e2e, desktop, harness, ci. **Requirements:** TBD **Plans:** 0 plans Plans: - [ ] TBD (promote with /gsd-review-backlog when ready)