From d136099dd88c34ccb040c4b65e9c4dcdb87973e7 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 15:15:18 -0400 Subject: [PATCH] wip: phase 5 web-push paused after discuss (ready to plan) --- .planning/HANDOFF.json | 35 +++++---- .../.continue-here.md | 74 +++++++++++++++++++ 2 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 .planning/phases/05-web-push-notifications/.continue-here.md diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index 14f85d1..c3f65c3 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -1,35 +1,34 @@ { "version": "1.0", - "timestamp": "2026-06-08T01:52:24.365Z", - "phase": "4", - "phase_name": "Shared Lists + Live Sync", - "phase_dir": ".planning/phases/04-shared-lists-live-sync", + "timestamp": "2026-06-09T19:14:03.727Z", + "phase": "5", + "phase_name": "Web Push Notifications", + "phase_dir": ".planning/phases/05-web-push-notifications", "plan": null, "task": null, "total_tasks": null, "status": "paused", - "stage": "discuss-complete; entry-gate-cleared; ready-to-plan", + "stage": "discuss-complete; ready-to-plan", "completed_tasks": [ - {"id": 1, "name": "discuss-phase 4 → 04-CONTEXT.md + 04-DISCUSSION-LOG.md (18 decisions D-01..D-18)", "status": "done", "commit": "05e1c9e"}, - {"id": 2, "name": "Ran SSE-over-Pangolin smoke test live — PASS (~6 min, 35 heartbeats, buffering off, no cut)", "status": "done"}, - {"id": 3, "name": "Recorded gate PASS via quick task 260607-u8o (01-HUMAN-UAT item 4 + 03-GATE2-RESULTS Part C)", "status": "done", "commit": "9ee5906"}, - {"id": 4, "name": "Saved project memory: N-member expansion principle", "status": "done"} + {"id": 1, "name": "discuss-phase 5 → 05-CONTEXT.md + 05-DISCUSSION-LOG.md (D-01..D-14, 3 areas discussed)", "status": "done", "commit": "e74f24d"}, + {"id": 2, "name": "Recorded STATE.md session (phase 5 context gathered)", "status": "done", "commit": "3bbfbbc"} ], "remaining_tasks": [ - {"id": 5, "name": "Plan Phase 4: /gsd-plan-phase 4 (reads 04-CONTEXT.md)", "status": "not_started"}, - {"id": 6, "name": "Optionally /gsd-ui-phase 4 for the lists UI design contract (UI hint: yes)", "status": "not_started"}, - {"id": 7, "name": "Execute Phase 4 plans", "status": "not_started"} + {"id": 3, "name": "Optional: /gsd-ui-phase 5 — UI design contract (permission prompt, settings toggle, notification flow)", "status": "not_started"}, + {"id": 4, "name": "/gsd-plan-phase 5 — research + plan (reads 05-CONTEXT.md → RESEARCH.md, PLAN.md files)", "status": "not_started"}, + {"id": 5, "name": "Execute Phase 5 plans", "status": "not_started"} ], "blockers": [], "human_actions_pending": [ - {"action": "Log out / invalidate the Authelia session whose oidc-auth cookie was pasted into the transcript during the SSE test", "context": "Cookie embeds a refresh token valid until ~2026-06-08; it is now in conversation history. Non-blocking but a security hygiene follow-up.", "blocking": false} + {"action": "Create + share the shared Family calendar and mark is_shared=1 on the primary Fastmail account", "context": "Phase 2 D-16 deferred. Reminders (NOTIF-01) fire ONLY on shared-calendar events (D-05), so live verification of reminders needs a real shared calendar with events. Implementation/planning is not blocked — the reminder path handles the empty-shared-calendar case correctly.", "blocking": false} ], "decisions": [ - {"decision": "Phase 4 entry gate (SSE-over-Pangolin, D-14/#1034) CLEARED", "rationale": "Live smoke test held a heartbeat stream open ~6 min through Pangolin with incremental delivery and no cut; buffering confirmed off", "phase": "4"}, - {"decision": "All Phase 4 implementation decisions captured in 04-CONTEXT.md (D-01..D-18)", "rationale": "Discuss-phase complete; downstream planner/researcher consume CONTEXT.md", "phase": "4"}, - {"decision": "Design member-count-agnostic (N family members, not hard-coded 2)", "rationale": "User stated future expansion; drives list_shares join table + per-list-scoped SSE fan-out", "phase": "4"} + {"decision": "Reminders fire for SHARED Family-calendar events ONLY (not personal)", "rationale": "Native device calendar apps already remind for personal events; FamilySync must not duplicate. Deliberately narrows literal NOTIF-01 — single most consequential decision; revisit if the household expects all-event reminders.", "phase": "5"}, + {"decision": "Coalesce list-change pushes per list; events specific / list pings generic; name the actor; meaningful event-changes only (description-only edits silent)", "rationale": "Anti-spam for the chatty list source; clear attribution for a two-person household.", "phase": "5"}, + {"decision": "Contextual permission prompt after install; single master toggle (v1); silent auto re-subscribe on dead subscription", "rationale": "Low-friction for the non-technical member; per-category opt-out deferred to v1.x.", "phase": "5"}, + {"decision": "Fixed 15-min lead time; no all-day reminders", "rationale": "MVP simplicity; custom/per-event lead time is v1.x.", "phase": "5"} ], "uncommitted_files": [], - "next_action": "/clear then /gsd-plan-phase 4 (entry gate is cleared; no infra precondition remains)", - "context_notes": "Phase 4 = app-native shared lists (MariaDB, NOT CalDAV) with SSE live co-edit sync. Discussion is done and committed; the SSE transport gate that was blocking the build is now verified and recorded. Nothing is mid-edit — clean stopping point between discuss and plan. Key locked choices for the planner: default-shared lists + private toggle via list_shares join table; checked items sink to completed section; optimistic UI + per-field PATCH last-write-wins (no CRDT); delete-wins; full-refetch-on-reconnect + capped-backoff + polling fallback; string fractional-index ordering; bottom tab bar + react-router. Open for planner: in-memory EventEmitter vs Redis pub/sub (single Node process today, ioredis not installed); new tables must use drizzle generate+migrate, NOT push." + "next_action": "/clear then /gsd-plan-phase 5 (CONTEXT.md is the source of truth; entry gates from Ph3/Ph4 already cleared).", + "context_notes": "Just finished discuss-phase for Phase 5 (Web Push, MVP mode, depends on Ph3+Ph4). 05-CONTEXT.md + 05-DISCUSSION-LOG.md written and committed; clean stopping point between discuss and plan, nothing mid-edit. Most consequential locked choice: D-05 shared-only reminder scope (deliberate NOTIF-01 narrowing — flagged for planner + verification). For the planner: the new push-subscription table MUST use drizzle generate+migrate (db:push unsafe on populated MariaDB); push dispatch reuses the existing in-memory listEmitter publish points (apps/api/src/lib/listEmitter.ts) + poller/outboxWorker change-detection; web-push and ioredis are NOT installed (VAPID keys need generating, no Redis — single Node process); SW is vite-plugin-pwa generateSW + autoUpdate so the push/notificationclick handler likely needs an injectManifest switch; iOS subscription health-check + event.waitUntil() + visible-notification-on-every-push are mandatory from day one (STATE.md)." } diff --git a/.planning/phases/05-web-push-notifications/.continue-here.md b/.planning/phases/05-web-push-notifications/.continue-here.md new file mode 100644 index 0000000..e024103 --- /dev/null +++ b/.planning/phases/05-web-push-notifications/.continue-here.md @@ -0,0 +1,74 @@ +--- +context: phase +phase: 05-web-push-notifications +task: null +total_tasks: null +status: paused +last_updated: 2026-06-09T19:14:03.727Z +--- + + +Phase 5 (Web Push Notifications, MVP mode, depends on Phase 3 + Phase 4) just +completed **discuss-phase**. `05-CONTEXT.md` and `05-DISCUSSION-LOG.md` are written +and committed (e74f24d); STATE.md session recorded (3bbfbbc). Working tree clean. + +This is a clean stop **between discuss and plan** — no plans/tasks exist yet, +nothing is mid-edit. Resume by planning the phase. + + + + +- Task 1: discuss-phase 5 → `05-CONTEXT.md` + `05-DISCUSSION-LOG.md` (decisions D-01..D-14). Done (e74f24d). +- Task 2: STATE.md session recorded. Done (3bbfbbc). + + + + +- Task 3 (optional): `/gsd-ui-phase 5` — UI design contract for the permission prompt, settings master toggle, and notification flow. This phase has real frontend surface. +- Task 4: `/gsd-plan-phase 5` — research + plan (reads `05-CONTEXT.md`). +- Task 5: execute the plans. + + + + +- **D-05 (most consequential): reminders fire for the SHARED Family calendar ONLY** — not personal events. Deliberate, because each member's native device calendar app already reminds for personal events and FamilySync must not duplicate. This narrows a literal reading of NOTIF-01; flagged for planner + verification. +- Coalesce list-change pushes per list; events show specifics, list pings stay generic (actor + list + count, no item text); name the actor; only meaningful event-changes push (description-only edits stay silent). +- Contextual permission prompt right after install; single master on/off toggle (v1); silent auto re-subscribe when a subscription dies but OS permission is still granted. +- Fixed 15-min lead; no all-day reminders. + + + +- None blocking planning/implementation. (Human action — non-blocking: the shared "Family" calendar must be created + shared + `is_shared=1` per Phase 2 D-16 before reminders have real events to fire on for live verification.) + + +## Required Reading (in order) +1. `.planning/phases/05-web-push-notifications/05-CONTEXT.md` — the source of truth; decisions, canonical refs, code-context, deferred items. +2. `CLAUDE.md` §"React PWA Stack" — iOS push constraints (16.4 min, install required, gesture subscribe, visible-notification mandatory) + web-push/VAPID stack entry. +3. `.planning/STATE.md` (Phase 5 note) — iOS revokes after ~3 silent pushes; health-check + `event.waitUntil()` mandatory. +4. `apps/api/src/lib/listEmitter.ts`, `apps/api/src/broker/poller.ts`, `apps/api/src/broker/outboxWorker.ts` — push-dispatch hook points + change-detection sources. + +## Critical Anti-Patterns (do NOT repeat these) +| Pattern | Description | Severity | Prevention Mechanism | +|---------|-------------|----------|---------------------| +| `db:push` on populated MariaDB | The new push-subscription table must NOT be created via `drizzle-kit push` — it emits a false destructive diff and can truncate tables on populated MariaDB | advisory | Use `drizzle-kit generate` + `migrate` for all new tables this phase | +| Silent pushes on iOS | Any push that does not display a visible notification counts toward iOS's ~3-strike silent-revocation; subscriptions die silently | advisory | Every push MUST show a visible notification; SW uses `event.waitUntil()`; implement the subscription health-check from day one | +| Introducing Redis for fan-out | The API is a single Node process; `ioredis` is not installed | advisory | Push dispatch reuses the in-memory `listEmitter` publish points — do not add Redis (matches Ph4 decision) | + +## Infrastructure State +- `web-push` and `ioredis`: NOT installed. VAPID keys not yet generated. No Redis (single Node process). +- Service worker: vite-plugin-pwa `generateSW` + `autoUpdate` — adding a `push`/`notificationclick` handler likely requires switching to `injectManifest` (planner's call; preserve Workbox precache + autoupdate). +- react-router installed (Ph4 D-17) — enables tap-to-open deep links (`/lists/:id`, event URLs). +- No running background services from this session. + + +Discussion is done and committed; nothing is in flight. Three gray areas were +discussed (Copy & anti-spam, Reminder scope & timing, Onboarding & opt-out); +Quiet-hours/DND was left to discretion (v1 = none). The shared-only reminder +scope (D-05) is the decision most worth re-examining before committing the plan +if the household's mental model is "remind me about everything." + + + +Start with: `/clear` then `/gsd-plan-phase 5` (optionally `/gsd-ui-phase 5` first +for the notification UI design contract). Read `05-CONTEXT.md` before acting. +