chore: archive v1.1 milestone files
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6cc3b8ae27
commit
7fbb3cca9d
+44
-503
@@ -3,7 +3,9 @@
|
||||
## 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–17 (planning) — mobile test harness, Gitea CI (runs the harness), faster write-back, in-app admin, per-event reminders, guided setup, real lint gate, desktop e2e, doc-only CI skip + markdown lint, CI dependency audit + security checks + image hygiene, UI optimization & polish
|
||||
- ✅ **v1.1 Operability & Polish** — Phases 7–20 (shipped 2026-06-18) — see [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md)
|
||||
|
||||
> Next milestone not yet defined — start with `/gsd-new-milestone`.
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -21,439 +23,52 @@ Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROA
|
||||
|
||||
</details>
|
||||
|
||||
### 🚧 v1.1 Operability & Polish (Phases 7–17)
|
||||
|
||||
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)
|
||||
- [x] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee (completed 2026-06-12)
|
||||
- [x] **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 (completed 2026-06-13)
|
||||
- [x] **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 (completed 2026-06-14)
|
||||
- [x] **Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface (completed 2026-06-16)
|
||||
- [x] **Phase 13: Real Lint Gate (ESLint)** - Wire ESLint flat config (typescript-eslint + React) across both apps so the Phase 8 CI lint slot actually fails on violations instead of no-op'ing (completed 2026-06-12)
|
||||
- [x] **Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile (completed 2026-06-12)
|
||||
- [x] **Phase 15: Doc-Only CI Skip + Markdown Lint** - Aggregate-gate the slow api/harness CI jobs so doc-only PRs to main merge without running them (no branch-protection deadlock), and add markdownlint to `fast-checks` so docs get a fast format+lint gate (promoted from backlog 999.17) (completed 2026-06-12)
|
||||
- [x] **Phase 16: CI Dependency Audit, Security Checks & Image Hygiene** - Extend Gitea CI with outdated-dependency reporting + vulnerability audit + a baseline of additional security checks, and enforce the dev/prod image boundary so no dev-bypass, secret, or family data ships in published images (absorbs backlog 999.17); independent of the admin chain (completed 2026-06-13)
|
||||
- [x] **Phase 17: UI Optimization & Polish** - Phone-layout polish + branding + theme groundwork: fix the long-standing phone-layout overlap where the fixed BottomTabBar covers the New Event FAB and the colour legend (+ small-viewport sweep), finish the branding assets (real FamilySync logo into the BrandSlot seam + a complete favicon/PWA-icon set replacing the placeholder stubs), and restructure tokens.css into a themeable token layer (light-only groundwork for future dark mode). Shipped dark theme → backlog 999.20; broader styling refresh → backlog 999.21 (future milestone) (completed 2026-06-18)
|
||||
|
||||
## 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)*
|
||||
|
||||
- [x] 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)*
|
||||
|
||||
- [x] 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)*
|
||||
|
||||
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> 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**: 2 plans (2 waves)
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 09-01-PLAN.md — TDD: outboxTrigger.ts (zero-dep EventEmitter signal) + scheduleOutboxDrain wrapper / drainRequested trailing-re-drain loop + initOutboxTrigger in outboxWorker.ts; trigger-wiring tests (SC-1, SC-4/D-07, D-05) (Wave 1)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 09-02-PLAN.md — Four post-commit signalOutboxDrain() publish sites in events.ts (create / edit-as-move-after-transaction / same-cal update / delete) + initOutboxTrigger() startup wiring under isMainModule() in index.ts (Wave 2)
|
||||
|
||||
### 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.
|
||||
|
||||
**Folded-in scope** (from backlog 999.5, self-service member onboarding): the credential surface this phase builds is the same one a member needs on first login. Expose a `needsProviderSetup` signal (member has no `member_credentials` row) and let a member enter/validate (CalDAV PROPFIND) + encrypt their **own** Fastmail app password — the self-service counterpart of the admin-managed flow, sharing the validation/encryption/initial-sync path. Non-technical-friendly instructions (link to Fastmail's app-password page, required Calendars/CalDAV scope) are the hard UX constraint. Member-scoped: a member can only set their own credential; never log/echo the password.
|
||||
|
||||
**Plans**: 4 plans (4 waves)Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 10-01-PLAN.md — v1.1 DB foundation migration (is_admin, provider_type+unique, reminder_lead_minutes, app_config) + dev-bypass admin seed
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 10-02-PLAN.md — requireAdmin guard + first-login-wins bootstrap + /api/me isAdmin/needsProviderSetup (TDD)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 completion)*
|
||||
|
||||
- [x] 10-04-PLAN.md — PWA /admin route + nav gating + CredentialSheet + SetupBanner (playwright-cli verified)
|
||||
|
||||
**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**: 4 plans (3 waves)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 11-01-PLAN.md — VALARM builders + classifier + extractor + computeAlertInstantUtc (vevent.ts, TDD)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 11-02-PLAN.md — Variable-lead scheduler: uid:dtstartMs dedup, drop isShared, all-day 9 AM, humanized body (TDD)
|
||||
- [x] 11-03-PLAN.md — Backend plumbing: schema field, outbox preserve-on-edit, sync upsert, occurrence surfacing
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 11-04-PLAN.md — EventForm reminder picker (allDay swap, edit pre-population) + client types + Playwright smoke
|
||||
|
||||
**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**: 7 plans in 4 waves (4 original + 3 gap-closure for 12-UAT.md gaps 1-6)
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 12-01-PLAN.md — Schema migration (nullable OIDC + claimed) + generate-secrets helper (SETUP-03) + Wave-0 scaffolds
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 12-02-PLAN.md — Pre-auth /api/setup/* router + isSetupLocked 423 guard + index mount + OIDC boot fallback (SETUP-01/02/04)
|
||||
- [x] 12-03-PLAN.md — First-login-claims rework in upsertUser (D-08, SETUP-01)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 12-04-PLAN.md — PWA SetupPage wizard + App.tsx gate + UI-SPEC revision (SETUP-01/02)
|
||||
|
||||
**Wave 4 — Gap closure** *(UAT 12-UAT.md gaps 1-6; 06+07 parallel, 05 blocked on 06)*
|
||||
|
||||
- [x] 12-06-PLAN.md — Backend: validate/vapid asserts wizard key == env VAPID_PUBLIC_KEY (gap 2) + status exposes non-secret DB name (gap 3) (SETUP-02)
|
||||
- [x] 12-07-PLAN.md — App.tsx: reverse-gate /setup post-completion (gap 5) + reconcile ['me'] so calendar banner clears after wizard (gap 6) (SETUP-01/04)
|
||||
- [x] 12-05-PLAN.md — SetupPage: drop DB-vs-env aside (gap 1) + read-only DB-name field (gap 3) + persist fields across Back (gap 4) (SETUP-01) — depends on 12-06
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 13: Real Lint Gate (ESLint)
|
||||
|
||||
**Goal**: The CI lint gate actually fails on lint violations. A real ESLint flat config (`eslint.config.js`, `typescript-eslint`; React + react-hooks plugins for `apps/pwa`) plus a package-level `lint` script in `apps/api` and `apps/pwa` makes the existing root `pnpm -r --if-present lint` run a real linter, replacing the hollow no-op gate that exits 0 because no linter exists.
|
||||
**Mode:** standard
|
||||
**Depends on**: Phase 8 (the CI `fast-checks` job already runs `pnpm lint`; this fills the slot Phase 8 shipped wired to auto-activate once a package `lint` script lands). Independent of all other phases.
|
||||
**Requirements**: TBD (promoted from backlog 999.16)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. `pnpm lint` runs ESLint across both `apps/api` and `apps/pwa` and exits non-zero on an introduced violation (verified by a deliberate test violation), where today it exits 0 with no linter present.
|
||||
2. The CI `fast-checks` lint step blocks a PR to main on lint violations — the gate can now fail.
|
||||
3. The first real run's existing violations are resolved (fix / warn / disable decided per rule) so the baseline gate ends green.
|
||||
|
||||
**Pitfalls this phase owns**:
|
||||
|
||||
- Pick a baseline ruleset (recommended vs strict-type-checked) deliberately — strict surfaces a large upfront cleanup; decide blocking vs advisory before flipping the gate to blocking.
|
||||
- `typecheck`/tsc already gates type errors; ESLint should not duplicate type-checking rules unnecessarily.
|
||||
|
||||
**Plans**: 3 plans — all complete (scope expanded during planning to add a Prettier `format:check` gate)
|
||||
|
||||
- [x] 13-01-PLAN.md — Install ESLint/Prettier deps + flat config + package scripts + prove the gate fails (SC-1)
|
||||
- [x] 13-02-PLAN.md — Fix all first-run lint violations across both apps, green `pnpm lint` (D-13-05/06)
|
||||
- [x] 13-03-PLAN.md — Prettier reformat (isolated commit) + CI format:check step + green baseline (SC-2/SC-3)
|
||||
|
||||
**UI hint**: no
|
||||
|
||||
### Phase 14: Desktop E2E Coverage
|
||||
|
||||
**Goal**: The Phase 8 regression gate exercises the desktop layout and flows, not just mobile. A `desktop` Playwright project (`devices['Desktop Chrome']`, no touch, wide viewport) is added to `apps/pwa/playwright.config.ts`, and the existing mobile-authored specs are reviewed/adjusted (or appropriately skipped) so `pnpm test:e2e` passes on a no-touch desktop viewport as well as the `iphone`/`pixel` profiles.
|
||||
**Mode:** standard
|
||||
**Depends on**: Phase 7 (the harness it extends) and Phase 8 (CI runs `pnpm test:e2e` and picks up the new project automatically — no CI plumbing change needed beyond any desktop-profile runtime/wait). Independent of Phases 9–13.
|
||||
**Requirements**: TBD (promoted from backlog 999.15)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. A `desktop` project exists in `playwright.config.ts` (Desktop Chrome, wide viewport, no `hasTouch`).
|
||||
2. The existing e2e specs pass (or are explicitly, justifiably skipped) on the desktop profile — touch-gesture / mobile-drawer / mobile-only-layout assumptions are handled.
|
||||
3. `pnpm test:e2e` in CI runs and gates on both mobile and desktop profiles (blocking-vs-advisory for desktop decided when planned).
|
||||
|
||||
**Pitfalls this phase owns**:
|
||||
|
||||
- The real work is the spec-compat pass, not CI plumbing — Phase 8 reused the Phase 7 harness unchanged, so the config addition is small but specs authored for touch/mobile need per-spec review.
|
||||
- Desktop WebKit is optional — the Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
||||
|
||||
**Plans**: 1 plan
|
||||
Plans:
|
||||
|
||||
- [x] 14-01-PLAN.md — Add the `desktop` Playwright project, desktop-skip the two mobile-only layout assertions (+ D-04 parity), update spec/README docs, and prove `pnpm test:e2e` is green on iphone + pixel + desktop with a blocking CI gate.
|
||||
|
||||
**UI hint**: no
|
||||
|
||||
### Phase 15: Doc-Only CI Skip
|
||||
|
||||
**Goal**: Doc-only PRs to `main` merge without running the slow `harness` (Playwright e2e + dev-stack bring-up, ~5 min) and `api` (MariaDB integration) jobs, while `fast-checks` (Prettier `format:check` + markdown linting) still runs — and branch protection never deadlocks on a required check that never reports. Docs get a *fast but real* gate: format + lint, none of the slow code jobs.
|
||||
**Mode:** standard
|
||||
**Depends on**: Phase 8 (the `.gitea/workflows/ci.yml` it modifies) and Phase 13 (the `fast-checks` job + `format:check` step this extends). Independent of Phases 9–12.
|
||||
**Requirements**: TBD (promoted from backlog 999.17)
|
||||
**Success Criteria** (what must be TRUE):
|
||||
|
||||
1. A doc-only PR to `main` (only `docs/` or `*.md` changed) skips the `api` and `harness` jobs but still runs `fast-checks`.
|
||||
2. A PR touching code runs `fast-checks`, `api`, and `harness` as today; a failure in any blocks the merge.
|
||||
3. Branch protection requires `CI / fast-checks` + an always-running `CI / gate` aggregate (passes when each heavy job is `success` OR `skipped`) — the direct `api`/`harness` requirements are dropped so a skipped heavy job never deadlocks the merge.
|
||||
4. `fast-checks` runs a markdown linter (markdownlint-cli2) over `**/*.md`; an introduced markdown-lint violation fails the gate, and the existing markdown baseline passes (violations fixed or rules configured) so the gate starts green.
|
||||
|
||||
**Pitfalls this phase owns**:
|
||||
|
||||
- **Required-check deadlock** — never path-filter a required context directly; a required job that never reports blocks the PR forever. The always-running `gate` job (`if: always()`, passes on `success`/`skipped`) is the only safe gating surface.
|
||||
- **Gitea skipped-status quirk** — Gitea may not emit a commit-status for a `skipped` job; rely on the always-running `gate`, not on marking `api`/`harness` skipped-but-required.
|
||||
- **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`.
|
||||
- **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files.
|
||||
|
||||
**Plans**: 3 plans (3 waves)
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3)
|
||||
|
||||
**UI hint**: no
|
||||
|
||||
### Phase 16: CI Dependency Audit, Security Checks & Image Hygiene
|
||||
|
||||
**Goal**: The CI pipeline surfaces outdated and vulnerable dependencies, runs a baseline of additional security checks, and enforces a clean dev↔prod boundary in the images it publishes — so the two-person household app doesn't silently rot on stale/CVE-bearing packages, and no dev-only affordance, secret, or family-specific data ever ships in a production image. Extends the existing Gitea CI (Phase 8) workflow with dependency/security/image-hygiene gates rather than standing up a separate pipeline. **Absorbs backlog 999.17 (dev/prod image boundary).**
|
||||
**Mode:** standard
|
||||
**Depends on**: Phase 8 (Gitea CI — adds steps to the existing workflow + publish job; no admin-chain dependency). Independent of Phases 10–12.
|
||||
**Requirements**: SEC-01 (secret scanning), SEC-02 (static security lint), DEP-01 (vuln audit gate), DEP-02 (outdated advisory), IMG-01 (NODE_ENV+boot-guard), IMG-02 (.dockerignore), IMG-03 (publish image-hygiene assertions), CI-03 (security job + gate wiring)
|
||||
|
||||
**Candidate scope (to be sharpened in `/gsd-discuss-phase 16`):**
|
||||
|
||||
- **Outdated dependencies:** a CI step that reports dependencies behind their latest (e.g. `pnpm outdated -r`), surfaced on the PR. Decide gating vs advisory, and how to handle the pinned-version table in CLAUDE.md (the stack pins exact versions — "outdated" must not fight intentional pins).
|
||||
- **Vulnerability audit:** `pnpm audit` (or equivalent) against the lockfile, failing on a chosen severity threshold (e.g. high/critical). Decide the threshold and an allowlist/waiver mechanism for unfixable transitive advisories.
|
||||
- **Additional security checks (user is open to these — pick a sensible baseline, avoid over-build):** candidates — secret scanning on the diff (gitleaks/trufflehog), a CodeQL/`eslint-plugin-security` static pass, dependency-review on PRs, Dockerfile/image scan (e.g. trivy) of the published image.
|
||||
- **Dev/prod boundary definition & enforcement (from 999.17):** the `DEV_AUTH_BYPASS` concept (and any dev-only affordance) must be provably confined to local dev — never to production, never baked into published images. Today the guard is runtime-only (`NODE_ENV !== 'production' && DEV_AUTH_BYPASS === 'true'` in `apps/api/src/auth/devBypass.ts`); add (a) explicit documentation of what "dev image" vs "shipped image" means, and (b) build-time / boot-time enforcement (a `production` image refuses to boot — or the build aborts — if dev-bypass is enabled) as defense-in-depth.
|
||||
- **No data/secrets in published images (from 999.17):** audit the Dockerfile(s) + the Phase 8 publish job (`publish.yml`) to confirm `.env`, dev seed SQL, local DB dumps, encryption keys, OIDC secrets, the `DEV_USER` seed, and family-specific fixtures are `.dockerignore`d and never `COPY`'d. Add a CI assertion that fails the publish if a dev-bypass code path is active, a forbidden env/secret is present, or personal/seed data is staged into the image context. The dev-stack seed path (`DEV_USER` id 1 + sample calendar/list data) must be unreachable from the production image/compose.
|
||||
- **Noise control:** these gates are notorious for flaky/advisory-churn failures; decide blocking-on-merge vs warn-only per check, and where results surface (PR annotation vs job log), mirroring Phase 15's gate-aggregation approach.
|
||||
|
||||
**Boundary:** Extends the existing Gitea CI workflow + publish job; does not remove dev-bypass (still needed for local verification and the Phase 7/8 harness) and does not add a new external service or a runtime dependency to the app. Automated dependency *upgrades* (e.g. Renovate/Dependabot bots) are a separate concern — decide in discuss whether they're in scope or deferred.
|
||||
|
||||
**Plans**: 6 plans in 2 waves
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 16-01-PLAN.md — Image-hygiene runtime: bake NODE_ENV=production + boot-time refuse-to-boot guard (IMG-01)
|
||||
- [x] 16-02-PLAN.md — pnpm audit gate + waiver allowlist + advisory-only tiered outdated report (DEP-01, DEP-02)
|
||||
- [x] 16-03-PLAN.md — Fold eslint-plugin-security into the lint gate as blocking errors + triage (SEC-02)
|
||||
- [x] 16-04-PLAN.md — gitleaks config + full-history baseline + .dockerignore (SEC-01, IMG-02)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 16-05-PLAN.md — Add the security job to ci.yml (gitleaks always; audit/outdated code-gated) + gate wiring (CI-03)
|
||||
- [x] 16-06-PLAN.md — publish.yml static image-hygiene assertion + boot-smoke before push (IMG-03)
|
||||
|
||||
**UI hint**: no
|
||||
|
||||
### Phase 17: UI Optimization & Polish
|
||||
|
||||
**Goal**: A visual-identity & polish pass for the PWA spanning three workstreams: **(A) phone-layout polish** so the phone (≤767px) layout has no fixed-chrome overlap and small-viewport spacing reads cleanly — starting with the long-standing BottomTabBar overlap that hides the New Event FAB and the colour legend, plus a small-viewport sweep; **(B) branding assets** — generate a real FamilySync logo into the existing `BrandSlot` seam (`apps/pwa/src/components/BrandSlot.tsx`) and a complete favicon/PWA-icon set replacing the placeholder stubs in `apps/pwa/public/`; **(C) theme-token groundwork** — restructure `apps/pwa/src/styles/tokens.css` into a themeable semantic-token layer (swappable by `data-theme`/`prefers-color-scheme`), light staying the only shipped theme, so a future dark theme is cheap.
|
||||
**Mode:** standard
|
||||
**Depends on**: Nothing structural (CSS/layout + assets only). Best sequenced after Phase 10 merges (the BottomTabBar gained an Admin tab and the new SetupBanner adds top pressure on phone), but otherwise independent of the admin chain.
|
||||
**Requirements**: No REQ-IDs — decisions D-01…D-10 (17-CONTEXT.md) stand in. Coverage: D-01/D-02 (A, phone overlap+guard) → 17-03; D-03/D-04 (B, assets) → 17-02; D-04/D-05 (B, wiring) → 17-04; D-06 (C, token groundwork) → 17-01; D-07/D-09 (D, logout+sheet centering) → 17-05; D-08/D-09/D-10 (D, admin toasts+reset-sheet+two-tab nav) → 17-06.
|
||||
**Scope boundary (set in `/gsd-discuss-phase 17`, 2026-06-17):** Workstream C ships token groundwork **only** — no dark palette, no theme toggle (→ backlog **999.20**). A broader "modern styling" visual refresh is **out of scope** and routed to backlog **999.21** (future milestone). Keep Phase 17 a focused polish + branding + groundwork pass, not a redesign.
|
||||
|
||||
**Seed defect — phone-layout bottom-bar overlap (documented 2026-06-13; long-standing, NOT introduced by Phase 10 — the BottomTabBar dates to Phase 04):**
|
||||
|
||||
At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx`) the layout switches to a 48px top AppNav + a `position: fixed` BottomTabBar (`height: calc(56px + env(safe-area-inset-bottom))`, z-index 200; `apps/pwa/src/components/BottomTabBar.tsx`) + a floating "New Event" FAB (`position: fixed; bottom: var(--space-6); right: var(--space-6)`; `apps/pwa/src/components/CalendarShell.tsx`). Two problems:
|
||||
|
||||
1. **FAB sits inside the bar** — the FAB's `bottom` offset (~`--space-6`, ≈24px) is smaller than the bar's 56px height, so the round New Event button overlaps the bottom tab bar (lands on the Admin tab).
|
||||
2. **Content occluded** — the content area (`contentStyle` in `App.tsx`) reserves no `padding-bottom` for the fixed bar, so the bottom of the calendar and the colour-legend chips (e.g. the "Dev User" / member legend) slide under the bar and are partially hidden.
|
||||
|
||||
**Fix sketch (CSS-only, no behaviour change):** on phone, lift the FAB to `bottom: calc(56px + env(safe-area-inset-bottom, 0px) + var(--space-6))` and add a matching `padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px))` to the phone content/scroll area (or reduce the `100dvh` column by the bar height). Verify across the `iphone`/`pixel`/`desktop` Playwright profiles and a real narrow Chromium via playwright-cli.
|
||||
|
||||
**Evidence:** reproduced 2026-06-13 with playwright-cli at 390×844 (FAB over the Admin tab; "Dev User" legend clipped) vs 1280×800 (desktop sidebar, no overlap). Full detail in todo `2026-06-13-pwa-phone-bottombar-overlap.md`.
|
||||
|
||||
**Candidate scope (to sharpen in `/gsd-discuss-phase 17`):** the seed defect above, plus a sweep for other small-viewport spacing / tap-target / overlap issues (the Phase 7 `layout.spec.ts` tap-target/overflow assertions are a ready checklist) and any phone/desktop visual inconsistencies noticed in use. Keep it a focused polish pass, not a redesign.
|
||||
|
||||
**Plans**: 6/6 plans complete
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 17-01-PLAN.md — C: tokens.css themeable-layer groundwork + --bottom-chrome-h token (D-06) [Wave 1]
|
||||
- [x] 17-02-PLAN.md — B: generate logo + full icon set, operator approval checkpoint (D-03, D-04) [Wave 1]
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 17-03-PLAN.md — A: phone FAB/BottomTabBar overlap fix + sweep + CI overlap assertion (D-01, D-02) [Wave 2, dep 01]
|
||||
- [x] 17-04-PLAN.md — B: wire logo into BrandSlot + index.html favicons + manifest maskable + accent (D-04, D-05) [Wave 2, dep 01,02]
|
||||
- [x] 17-05-PLAN.md — D: logout control + sheet desktop-centering (SettingsSheet/CredentialSheet) (D-07, D-09) [Wave 2, dep 01]
|
||||
- [x] 17-06-PLAN.md — D: admin success toasts + two-tab ARIA nav + reset-sheet centering + admin.spec.ts (D-08, D-09, D-10) [Wave 2, dep 01]
|
||||
|
||||
**UI hint**: yes
|
||||
<details>
|
||||
<summary>✅ v1.1 Operability & Polish (Phases 7–20) — SHIPPED 2026-06-18</summary>
|
||||
|
||||
- [x] Phase 7: Mobile Test Harness (4/4 plans) — completed 2026-06-11
|
||||
- [x] Phase 8: Gitea CI (4/4 plans) — completed 2026-06-11
|
||||
- [x] Phase 9: Faster Write-Back (2/2 plans) — completed 2026-06-12
|
||||
- [x] Phase 10: Admin Role & Settings (4/4 plans) — completed 2026-06-13
|
||||
- [x] Phase 11: Per-Event Reminders (5/5 plans) — completed 2026-06-14
|
||||
- [x] Phase 12: Initial Setup Wizard (7/7 plans) — completed 2026-06-16
|
||||
- [x] Phase 13: Real Lint Gate (ESLint) (3/3 plans) — completed 2026-06-12
|
||||
- [x] Phase 14: Desktop E2E Coverage (1/1 plans) — completed 2026-06-12
|
||||
- [x] Phase 15: Doc-Only CI Skip + Markdown Lint (3/3 plans) — completed 2026-06-12
|
||||
- [x] Phase 16: CI Dependency Audit, Security & Image Hygiene (6/6 plans) — completed 2026-06-13
|
||||
- [x] Phase 17: UI Optimization & Polish (6/6 plans) — completed 2026-06-18
|
||||
- [x] Phase 18: Auto Timezone Detection (4/4 plans) — completed 2026-06-14
|
||||
- [x] Phase 19: Local Auth (No-OIDC Mode) (5/5 plans) — completed 2026-06-17
|
||||
- [x] Phase 20: Admin Member Editor & Form Declutter (3/3 plans) — completed 2026-06-18
|
||||
|
||||
Full phase detail archived in [`milestones/v1.1-ROADMAP.md`](milestones/v1.1-ROADMAP.md).
|
||||
|
||||
</details>
|
||||
|
||||
## 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 | 4/4 | Complete | 2026-06-11 |
|
||||
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
||||
| 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 |
|
||||
| 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 |
|
||||
| 12. Initial Setup Wizard | v1.1 | 7/7 | Complete | 2026-06-16 |
|
||||
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
||||
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
||||
| 17. UI Optimization & Polish | v1.1 | 6/6 | Complete | 2026-06-18 |
|
||||
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
||||
| 19. Local Auth (No-OIDC Mode) | v1.1 | 5/5 | Complete | 2026-06-17 |
|
||||
| 20. Admin Member Editor & Declutter | v1.1 | 3/3 | Complete | 2026-06-18 |
|
||||
| 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 | 4/4 | Complete | 2026-06-11 |
|
||||
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
|
||||
| 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 |
|
||||
| 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 |
|
||||
| 12. Initial Setup Wizard | v1.1 | 7/7 | Complete | 2026-06-16 |
|
||||
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
||||
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||
| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 |
|
||||
| 17. UI Optimization & Polish | v1.1 | 6/6 | Complete | 2026-06-18 |
|
||||
| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 |
|
||||
| 19. Local Auth (No-OIDC Mode) | v1.1 | 5/5 | Complete | 2026-06-17 |
|
||||
| 20. Admin Member Editor & Declutter | v1.1 | 3/3 | Complete | 2026-06-18 |
|
||||
|
||||
## Backlog
|
||||
|
||||
@@ -696,77 +311,3 @@ Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
|
||||
### Phase 18: Auto timezone detection and ability to change timezone
|
||||
|
||||
**Goal:** Make the household timezone an explicit, stored, user-changeable setting — auto-detected from the browser at first run, changeable from the role-gated /admin Settings — and route the server-side all-day "9 AM local" reminder computation through it (replacing the implicit `process.env.TZ` fallback), without touching the already-correct browser-local display/timed-write path.
|
||||
**Requirements**: TBD (decision contract D-01..D-07 from 18-CONTEXT.md)
|
||||
**Depends on:** Phase 10 (admin role + `/admin` Settings + `app_config`); Phase 11 (all-day reminder computation this rewires). Independent of Phase 17. Phase 12 (setup wizard) not required — seeding is self-contained.
|
||||
**Plans:** 4/4 plans complete
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 18-01-PLAN.md — TDD: getHouseholdTimezone(db) accessor + isValidIanaTimezone (D-05/D-06)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04)
|
||||
- [x] 18-03-PLAN.md — TDD: route all-day reminder TZ at reminderScheduler:247 + outboxWorker:501,607 through the accessor (D-05/D-06/D-07)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [x] 18-04-PLAN.md — PWA Timezone section in /admin Settings (searchable IANA picker + detected-zone seed) + client fns (D-02/D-04)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 19: Local Auth (No-OIDC Mode)
|
||||
|
||||
**Goal:** Let an operator run FamilySync entirely on **local DB users with no OIDC** — username/password accounts and a local login flow that coexists with the Authelia OIDC path — and **optionally wire OIDC in later** by claiming/linking an existing local user to an OIDC identity. Removes the hard dependency on a deployed Authelia for small/solo self-hosters.
|
||||
**Mode:** standard
|
||||
**Depends on:** Phase 12 (Initial Setup Wizard) — builds directly on the pre-OIDC **local-user foundation** introduced there: nullable `users.oidc_iss`/`oidc_sub` + the claimed/pending marker, and the first-login-claims merge. Phase 19 generalizes that single bootstrap local user into a full local-account model + login.
|
||||
**Requirements**: AUTH-LOCAL-01..AUTH-LOCAL-20 (derived during planning 2026-06-17) — local_credentials schema (01), scrypt hash/verify (02), login route (03), localAuthMiddleware (04), auth-mode endpoint (05), logout (06), admin create-member (07), admin reset (08), self-change (09), OIDC-link (10), break-glass CLI (11), LoginPage (12), admin UI (13), settings UI (14), routing gate (15), dev-bypass/harness rework (16), hasLocalCredential (17), de-Authelia copy (18), rate-limit/lockout (19), auth unit tests (20). Plus `LOCAL_SESSION_SECRET` env + boot assertion (D-05).
|
||||
**Plans:** 5/5 plans complete
|
||||
|
||||
**Provenance:** Deferred from the Phase 12 discussion (2026-06-15) — see `.planning/phases/12-initial-setup-wizard/12-CONTEXT.md` §Deferred Ideas. The operator runs FamilySync this way themselves and wants no-OIDC operation as a first-class mode.
|
||||
|
||||
**Open questions for discuss/spec:**
|
||||
|
||||
- Password hashing/storage choice (e.g. argon2id/bcrypt) and how it sits alongside the env-only secret kernel from Phase 12.
|
||||
- How local login coexists with `oidcAuthMiddleware` ordering in `apps/api/src/index.ts` (route-level auth strategy selection vs. a mode flag in `app_config`).
|
||||
- The OIDC-link flow: claiming an existing local user into an `oidc_iss+oidc_sub` identity without violating the D-10 "identity is OIDC, never email" rule.
|
||||
- Whether "local mode vs OIDC mode" is a deploy-time switch or both can be live simultaneously.
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 19-01-PLAN.md — Foundation (TDD): local_credentials schema + 0003 migration, scrypt hash/verify, local-session JWT helpers, LOCAL_SESSION_SECRET boot guard + generate-secrets, .dockerignore scripts exclusion (AUTH-LOCAL-01/02)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1)*
|
||||
|
||||
- [x] 19-02-PLAN.md — Backend account mgmt (TDD): admin create/reset member, self-change password, hasLocalCredential, linkOidcToUser helper + /api/me/link-oidc (AUTH-LOCAL-07/08/09/10/17)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2)*
|
||||
|
||||
- [x] 19-03-PLAN.md — Middleware + routes + wiring (TDD): localAuthMiddleware, /api/auth/mode, login (rate-limit/lockout) + logout, index.ts mount + OIDC-guard skip + /callback link branch, de-Authelia comments (AUTH-LOCAL-03/04/05/06/18/19/20)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3; 04 + 05 parallel)*
|
||||
|
||||
- [x] 19-04-PLAN.md — PWA: LoginPage + BrandSlot + App.tsx gate + client.ts + AdminPage + SettingsSheet (AUTH-LOCAL-12/13/14/15)
|
||||
- [x] 19-05-PLAN.md — Dev-bypass Option C + break-glass CLI + harness/CI rework + login.spec.ts (AUTH-LOCAL-11/16)
|
||||
|
||||
### Phase 20: Admin Member Editor & Form Declutter
|
||||
|
||||
**Goal:** Replace the per-member-row action buttons (Rotate/Add credential + Reset password) in the admin Members panel with a single edit affordance — clicking a member's name or an edit button opens a member-detail editor where an admin modifies all of that member's details in one place: display name, local-login password, and the Fastmail/CalDAV app password (calendar credential) — using clear, non-jargon labels that retire the confusing "Rotate" term. Also collapse the "Add member" section so its input fields are hidden behind a single "Add member" trigger by default, decluttering the panel. Client-side AdminPage + CredentialSheet rework over the existing `/api/admin` endpoints; no new auth/authorization boundary (seeded by the gripe that "Rotate" for the app password is not intuitive).
|
||||
**Requirements**: TBD (refine in /gsd-discuss-phase 20 — open scope: which fields count as "all" (color swatch? admin toggle? OIDC link?), whether to keep any standalone reset-password flow, and the exact edit affordance — clickable name vs. row edit button)
|
||||
**Depends on:** Phase 19
|
||||
**Plans:** 3/3 plans complete
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [x] 20-01-PLAN.md — Server: PATCH /api/admin/members/:id (displayName + is_admin) with last-admin demotion guard (TDD) + isAdmin in GET /members
|
||||
- [x] 20-02-PLAN.md — PWA API client: AdminMember.isAdmin field + updateMemberProfile fetcher (last-admin sentinel)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [x] 20-03-PLAN.md — PWA: unified MemberEditorSheet (edit/create, per-section saves) + decluttered tappable Members panel; retire Rotate/Reset-password buttons
|
||||
|
||||
Reference in New Issue
Block a user