From 1cc08f1bf17eb7e5519b435cde5488dfb11d25f4 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 20:52:23 -0400 Subject: [PATCH] docs(05-01): complete Wave-0 foundation plan --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 18 ++- .../05-01-SUMMARY.md | 146 ++++++++++++++++++ 3 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 .planning/phases/05-web-push-notifications/05-01-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9364c95..f6e11f8 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -189,7 +189,7 @@ Plans: Plans: **Wave 1** -- [ ] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12) +- [x] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12) **Wave 2** *(blocked on Wave 1)* @@ -246,7 +246,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple | 2. Calendar Display | 5/5 | Complete | 2026-06-05 | | 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 | | 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 | -| 5. Web Push Notifications | 0/8 | Planned | - | +| 5. Web Push Notifications | 1/8 | In Progress| | | 6. UX Polish | 0/? | Not started | - | ## Backlog @@ -255,7 +255,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple **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:** 6/6 plans complete +**Plans:** 1/8 plans executed Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index 8760219..4fc7ac0 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Phase 5 UI-SPEC approved -last_updated: "2026-06-10T00:34:28.043Z" +stopped_at: Completed 05-01-PLAN.md +last_updated: "2026-06-10T00:52:12.517Z" last_activity: 2026-06-10 -- Phase 05 execution started progress: total_phases: 15 completed_phases: 4 total_plans: 36 - completed_plans: 28 + completed_plans: 29 percent: 27 --- @@ -26,8 +26,8 @@ See: .planning/PROJECT.md (updated 2026-06-07) ## Current Position Phase: 05 (web-push-notifications) — EXECUTING -Plan: 1 of 8 -Status: Executing Phase 05 +Plan: 2 of 8 +Status: Ready to execute Last activity: 2026-06-10 -- Phase 05 execution started Progress: [██████████] 100% @@ -65,6 +65,7 @@ Progress: [██████████] 100% | Phase 04 P05 | 10 | 2 tasks | 4 files | | Phase 04 P06 | 11 | 2 tasks | 7 files | | Phase 04 P07 | 6 | 2 tasks | 4 files | +| Phase 05 P01 | 20 | 4 tasks | 15 files | ## Accumulated Context @@ -100,6 +101,7 @@ Recent decisions affecting current work: - [Phase ?]: LIST-04: SSE connection lives in ListDetail (not hoisted to Lists route); Phase 5 push will own session lifecycle - [Phase 04-07]: D-04-07-collation: Drizzle 0.45.x has no first-class collation option on varchar; used customType to emit varchar(255) COLLATE utf8mb4_bin for list_items.rank — keeps schema-as-code + generate+migrate workflow - [Phase 04-07]: D-04-07-guard: isShared owner-only guard placed after access check, before updateValues construction; mirrors DELETE handler idiom (if !access.isOwner → 403) +- [Phase ?]: VAPID config is env-injected at runtime via docker-compose.yml environment block; no key baked into image (Phase 5 D-transposability) ### Roadmap Evolution @@ -141,6 +143,6 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-09T22:04:34.038Z -Stopped at: Phase 5 UI-SPEC approved -Resume file: .planning/phases/05-web-push-notifications/05-UI-SPEC.md +Last session: 2026-06-10T00:52:12.510Z +Stopped at: Completed 05-01-PLAN.md +Resume file: None diff --git a/.planning/phases/05-web-push-notifications/05-01-SUMMARY.md b/.planning/phases/05-web-push-notifications/05-01-SUMMARY.md new file mode 100644 index 0000000..de8be4b --- /dev/null +++ b/.planning/phases/05-web-push-notifications/05-01-SUMMARY.md @@ -0,0 +1,146 @@ +--- +phase: 05-web-push-notifications +plan: 01 +subsystem: api/push-foundation +tags: [web-push, vapid, schema, migration, test-scaffolds, red-tests] +dependency_graph: + requires: [04-shared-lists-live-sync] + provides: [push_subscriptions table, calendar_events.title column, Wave-0 RED test scaffolds, VAPID env wiring] + affects: [apps/api/src/db/schema.ts, apps/api/src/db/migrations/, apps/api/test/setup.ts, docker-compose.yml] +tech_stack: + added: [web-push@3.6.7, "@types/web-push@3.6.4", workbox-core@7.4.1, workbox-precaching@7.4.1, workbox-routing@7.4.1] + patterns: [drizzle-kit generate+migrate (never push), mysqlTable FK+unique+index pattern, RED test scaffold pattern] +key_files: + created: + - apps/api/src/db/migrations/0003_same_xavin.sql + - apps/api/src/db/migrations/meta/0003_snapshot.json + - apps/api/tests/fixtures/vapid.ts + - apps/api/tests/lib/pushDispatcher.test.ts + - apps/api/tests/lib/pushCoalescer.test.ts + - apps/api/tests/broker/reminderScheduler.test.ts + - apps/api/tests/lib/eventChangeDispatcher.test.ts + - apps/api/tests/routes/push.test.ts + - .env.example + modified: + - apps/api/src/db/schema.ts + - apps/api/test/setup.ts + - apps/api/package.json + - apps/pwa/package.json + - pnpm-lock.yaml + - docker-compose.yml +decisions: + - "VAPID config is env-injected at runtime (docker-compose.yml environment block); no key baked into image" + - "pushSubscriptions endpoint column uses text (not varchar) — push endpoints can exceed 512 chars" + - "calendarEvents.title is nullable varchar(500); pre-existing rows stay NULL until Phase 5 sync update" + - "Test VAPID keypair inlined in tests/fixtures/vapid.ts for offline-safe unit tests" +metrics: + duration: 20 + completed_date: "2026-06-10" + tasks_completed: 4 + files_changed: 15 +--- + +# Phase 05 Plan 01: Wave-0 Foundation Summary + +Web Push Wave-0 foundation: push dependencies installed, VAPID keypair env-injected, push_subscriptions table + calendar_events.title migrated, five RED test scaffolds committed. + +## Tasks Executed + +### Task 1: Package legitimacy gate + install push dependencies +**Status:** Done by orchestrator before this agent spawned. + +Installed packages verified in package.json: +- `apps/api`: web-push@^3.6.7 (prod), @types/web-push@^3.6.4 (dev) +- `apps/pwa`: workbox-core@^7.4.1, workbox-precaching@^7.4.1, workbox-routing@^7.4.1 (dev) + +Commit: `80bbdc1` — `chore(05-01): install web-push and workbox push dependencies` + +### Task 2: Generate VAPID keypair + record in env +**Status:** Done by orchestrator before this agent spawned. + +VAPID keypair generated and stored in gitignored `.env` (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT). Real keys never committed. + +(No dedicated commit — keys in .env only; .env.example documenting placeholders committed in Task 3.) + +### Task 3: Schema — push_subscriptions table + calendar_events.title, generate+migrate +**Status:** Completed. + +Added `pushSubscriptions` mysqlTable to `apps/api/src/db/schema.ts`: +- `user_id` INT NOT NULL FK → users.id ON DELETE CASCADE +- `endpoint` TEXT NOT NULL (globally unique — `uniq_push_endpoint`) +- `p256dh` TEXT NOT NULL +- `auth` VARCHAR(256) NOT NULL +- `created_at`, `updated_at` TIMESTAMP +- Index `idx_push_subscriptions_user_id` on userId + +Added `title` VARCHAR(500) (nullable) to `calendarEvents` after `rawVevent`. Populated from VEVENT SUMMARY by sync.ts in Plan 05-07; required for readable reminder/change copy (D-02/NOTIF-01). + +Migration generated via `db:generate` and applied via `db:migrate` (NOT `db:push` — anti-pattern per drizzle-mariadb-push-unsafe memory). Migration file: `0003_same_xavin.sql`. + +VAPID container-transposability: added VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT to `docker-compose.yml` api `environment:` block using `${VAR}` syntax (no default — must be set). Created root `.env.example` documenting all environment variables including VAPID vars with placeholders and generation instructions. + +Commit: `73fcdaf` — `feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring` + +### Task 4: Wave-0 RED test scaffolds + VAPID fixture + setup truncation +**Status:** Completed. + +Created `tests/fixtures/vapid.ts` — exports `TEST_VAPID` const with a statically inlined P-256 keypair (generated once; no runtime network call; offline-safe). + +Created five RED test scaffolds (all fail on `Cannot find module` — correct RED state): + +1. **tests/lib/pushDispatcher.test.ts** — 4 tests: 410/404 prune DELETE, 201 no-delete, 5xx no-delete +2. **tests/lib/pushCoalescer.test.ts** — 3 tests: burst collapses to 1 dispatch with count=N; excludeUserId passed; separate lists are independent +3. **tests/broker/reminderScheduler.test.ts** — 3 tests: all-day excluded (D-07); non-shared excluded (D-05); (uid,minuteBucket) dedup +4. **tests/lib/eventChangeDispatcher.test.ts** — 4 tests: create fires; title-change fires; description-only silent (D-04); actor excluded (D-03) +5. **tests/routes/push.test.ts** — POST 201/401; DELETE removes rows; GET /api/push/vapid-public-key returns `{ publicKey }` + +Updated `test/setup.ts`: +- Added `pushSubscriptions` to import from schema +- Added `await db.delete(pushSubscriptions)` in afterEach (before lists delete; no FK to lists) + +Commit: `ef558b6` — `test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation` + +## Deviations from Plan + +### Auto-added: VAPID container-transposability (orchestrator requirement) + +The orchestrator folded in a requirement not in the original plan: VAPID env vars must be env-injected in docker-compose.yml, not baked into the image. + +- **Fix:** Added three `${VAPID_*}` entries to `docker-compose.yml` api `environment:` block (no default fallback — unset = container won't start, which is correct: no VAPID = no push). +- **Also created:** Root `.env.example` (the plan listed it in `files_modified` but it didn't exist yet) documenting all environment variables for the project including VAPID. +- **Files modified:** docker-compose.yml, .env.example (created) + +### Package dependencies committed separately (Rule 3 — blocking issue) + +Tasks 1/2 package installs were done by the orchestrator but not yet committed (uncommitted changes in `apps/api/package.json`, `apps/pwa/package.json`, `pnpm-lock.yaml`). These were staged and committed as a separate chore commit (`80bbdc1`) before the schema commit, to keep dependency changes isolated from schema changes. + +## Known Stubs + +None. This plan lays only schema and test scaffolds — no UI rendering or data-flow stubs. + +## Threat Flags + +No new threat surface introduced. VAPID private key is in gitignored `.env` only; `.env.example` contains placeholders only (T-05-01 mitigated). Migration used generate+migrate workflow (T-05-02 mitigated). Package installs were pre-approved by human checkpoint Task 1 (T-05-SC mitigated). + +## Self-Check + +**Files created/verified:** + +- [x] apps/api/src/db/migrations/0003_same_xavin.sql — exists +- [x] apps/api/tests/fixtures/vapid.ts — exists +- [x] apps/api/tests/lib/pushDispatcher.test.ts — exists +- [x] apps/api/tests/lib/pushCoalescer.test.ts — exists +- [x] apps/api/tests/broker/reminderScheduler.test.ts — exists +- [x] apps/api/tests/lib/eventChangeDispatcher.test.ts — exists +- [x] apps/api/tests/routes/push.test.ts — exists +- [x] .env.example — exists + +**Commits verified:** +- 80bbdc1: chore(05-01): install web-push and workbox push dependencies +- 73fcdaf: feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring +- ef558b6: test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation + +**Typecheck:** passes (`pnpm --filter @familysync/api typecheck` — no errors) +**RED tests:** all 5 scaffold files fail on `Cannot find module` (correct; implementations in Plans 05-02..05-06) + +## Self-Check: PASSED