- 🚧 **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
- 🚧 **v1.1 Operability & Polish** — Phases 7–14 (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
## Phases
## Phases
@@ -21,7 +21,7 @@ Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROA
</details>
</details>
### 🚧 v1.1 Operability & Polish (Phases 7–12)
### 🚧 v1.1 Operability & Polish (Phases 7–14)
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.
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.
@@ -31,6 +31,8 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- [ ]**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 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 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 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
- [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)
- [ ]**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
## Phase Details
## Phase Details
@@ -60,11 +62,11 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
@@ -90,7 +92,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- **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.
- **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.
- **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.
- **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.
- **--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).
- 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).
@@ -99,15 +101,15 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
@@ -155,6 +157,8 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- **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*`.
- **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.
- 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**: TBD
**Plans**: TBD
**UI hint**: yes
**UI hint**: yes
@@ -208,34 +212,53 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
### Phase 13: Real Lint Gate (ESLint)
### Phase 13: Real Lint Gate (ESLint)
**Goal**: The CI `fast-checks` lint step stops being a hollow no-op — a real, type-aware ESLint flat config (typescript-eslint `recommendedTypeChecked` + React/react-hooks for the PWA) plus Prettier are wired across both apps, all first-run violations are genuinely fixed, and `pnpm lint` / `pnpm format:check` actually fail on violations and gate PRs to main.
**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
**Mode:** standard
**Depends on**: Phase 8 (Gitea CI shipped the lint gate slot wired to auto-activate once a package `lint` script lands; this phase makes it real and adds the format gate). Independent of Phases 9/10/11/12.
**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**: none (promoted from backlog 999.16; no formal REQ-IDs)
**Requirements**: TBD (promoted from backlog 999.16)
**Success Criteria** (what must be TRUE):
**Success Criteria** (what must be TRUE):
1.**SC-1:**`pnpm lint` exits non-zero on an introduced violation (today it exits 0); `pnpm format:check` exits non-zero on an unformatted file.
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.**SC-2:** The CI `fast-checks` lint step blocks a PR to main on lint violations, and a new `format:check` step blocks on format violations.
2. The CI `fast-checks` lint step blocks a PR to main on lint violations — the gate can now fail.
3.**SC-3:** The first real run's existing violations are resolved so the baseline gate ends green — `pnpm lint` AND `pnpm format:check` both exit 0 across both apps.
3. The first real run's existing violations are resolved (fix / warn / disable decided per rule) so the baseline gate ends green.
**Decisions** (LOCKED, from 13-CONTEXT.md): D-13-01 `recommendedTypeChecked` via `projectService:true`; D-13-02 React/hooks plugins scoped to `apps/pwa` only; D-13-03 no strict presets; D-13-04 `--max-warnings 0`; D-13-05 fix all violations now; D-13-06 fixes address, never mask (justified suppressions only); D-13-07 Prettier standalone + `eslint-config-prettier`; D-13-08 isolated reformat commit; D-13-09 lint all TS/TSX incl. tests/e2e/config; D-13-10 `disableTypeChecked` override for non-project files.
**Pitfalls this phase owns**:
**Plans**: 3 plans
**Wave 1**
- 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-02-PLAN.md — Fix all first-run lint violations across both apps, green `pnpm lint` (D-13-05/06)
- [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)
**Wave 3**_(blocked on Wave 2 completion)_
**UI hint**: no
- [x] 13-03-PLAN.md — Prettier reformat (isolated commit) + CI format:check step + green baseline (SC-2, SC-3)
### 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.
| 14. Desktop E2E Coverage | v1.1 | 0/? | Not started | - |
## Backlog
## Backlog
@@ -256,7 +280,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
**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.
**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.
**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.
**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 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).
@@ -282,31 +306,6 @@ Plans:
- [ ] TBD (promote with /gsd-review-backlog when ready)
- [ ] 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)
### 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:
**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:
@@ -356,7 +355,7 @@ Plans:
- **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.)
- **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.
- 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.
**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.
**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.
@@ -431,6 +430,8 @@ Plans:
**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.
**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.
> **Promoted into v1.1 Phase 14 (Desktop E2E Coverage) — 2026-06-11.** Backlog entry retained for history.
**Requirements:** TBD
**Requirements:** TBD
**Plans:** 0 plans
**Plans:** 0 plans
@@ -453,7 +454,7 @@ Plans:
**Context:** Raised during Phase 8 execution, 2026-06-11 — user noted the `--if-present` lint step "didn't fix the linter, just made it so it didn't have to exist to proceed" and wants a lint gate that actually fails. Tags: ci, lint, eslint, typescript-eslint, quality, gitea.
**Context:** Raised during Phase 8 execution, 2026-06-11 — user noted the `--if-present` lint step "didn't fix the linter, just made it so it didn't have to exist to proceed" and wants a lint gate that actually fails. Tags: ci, lint, eslint, typescript-eslint, quality, gitea.
> **Promoted into v1.1 Phase 13 (Real Lint Gate (ESLint)) — no formal REQ-IDs; scope expanded to include a Prettier `format:check` gate.** Backlog entry retained for history.
> **Promoted into v1.1 Phase 13 (Real Lint Gate / ESLint) — 2026-06-11.** Backlog entry retained for history.
-- NOTE (IN-01): UNIQUE(endpoint) with endpoint varchar(2048)/utf8mb4 is ~8192 bytes,
-- over InnoDB's 3072-byte index-key limit. Succeeds only because MariaDB 11 silently
-- builds over-length UNIQUE constraints as long-unique HASH indexes; this same DDL
-- fails on MySQL 8 or with stricter SQL modes. Engine-pinned to MariaDB (project hard
-- constraint). Already applied on main/production — do not alter.
CONSTRAINT`uniq_push_endpoint`UNIQUE(`endpoint`)
CONSTRAINT`uniq_push_endpoint`UNIQUE(`endpoint`)
);
);
--> statement-breakpoint
--> statement-breakpoint
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.