From 4ef6333201c07b818d0503d9aaa7155e23a32d4c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 21:53:33 -0400 Subject: [PATCH] docs(05-08): complete opt-out + reliability surface plan --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 11 +- .../05-08-SUMMARY.md | 193 ++++++++++++++++++ 3 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 .planning/phases/05-web-push-notifications/05-08-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index b016383..3cec9ce 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -211,7 +211,7 @@ Plans: **Wave 6** *(blocked on Wave 3)* -- [ ] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet +- [x] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet **UI hint**: yes @@ -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 | 6/8 | In Progress| | +| 5. Web Push Notifications | 7/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/8 plans executed +**Plans:** 7/8 plans executed Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index c48c796..8c349ef 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,15 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: executing +status: verifying stopped_at: Completed 05-06-PLAN.md -last_updated: "2026-06-10T01:40:01.911Z" +last_updated: "2026-06-10T01:53:17.783Z" last_activity: 2026-06-10 progress: total_phases: 15 completed_phases: 4 total_plans: 36 - completed_plans: 34 + completed_plans: 35 percent: 27 --- @@ -27,7 +27,7 @@ See: .planning/PROJECT.md (updated 2026-06-07) Phase: 05 (web-push-notifications) — EXECUTING Plan: 8 of 8 -Status: Ready to execute +Status: Phase complete — ready for verification Last activity: 2026-06-10 Progress: [█████████░] 89% @@ -71,6 +71,7 @@ Progress: [█████████░] 89% | Phase 05 P04 | 11 | 3 tasks | 9 files | | Phase 05 P05 | 8 | 2 tasks | 4 files | | Phase 05 P06 | 6 | 1 tasks | 2 files | +| Phase 05 P08 | 9 | 3 tasks | 7 files | ## Accumulated Context @@ -152,6 +153,6 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-10T01:40:01.904Z +Last session: 2026-06-10T01:53:17.776Z Stopped at: Completed 05-06-PLAN.md Resume file: None diff --git a/.planning/phases/05-web-push-notifications/05-08-SUMMARY.md b/.planning/phases/05-web-push-notifications/05-08-SUMMARY.md new file mode 100644 index 0000000..1725029 --- /dev/null +++ b/.planning/phases/05-web-push-notifications/05-08-SUMMARY.md @@ -0,0 +1,193 @@ +--- +phase: 05-web-push-notifications +plan: 08 +subsystem: pwa/hooks, pwa/components +tags: [web-push, settings, permission-denied, toggle, reliability, D-09, D-10] +dependency_graph: + requires: [05-04] + provides: [SettingsSheet (master toggle D-09), PermissionDeniedBanner (D-10), usePushSubscription setEnabled/isSubscribed, silent re-subscribe health-check (D-10)] + affects: + - apps/pwa/src/hooks/usePushSubscription.ts + - apps/pwa/src/components/SettingsSheet.tsx + - apps/pwa/src/components/PermissionDeniedBanner.tsx + - apps/pwa/src/components/AppNav.tsx + - apps/pwa/src/components/CalendarShell.tsx + - apps/pwa/src/App.tsx + - apps/pwa/src/styles/tokens.css +tech_stack: + added: [] + patterns: + - usePushSubscription setEnabled master toggle (D-09) + - Silent dead-subscription recovery on mount (D-10) + - PermissionDeniedBanner role=alert, OS-revoked-only gate + - SettingsSheet bottom sheet (role=dialog, z:301, Escape+backdrop close) + - AppNav avatar promoted to button with onOpenSettings prop chain +key_files: + created: + - apps/pwa/src/components/SettingsSheet.tsx + - apps/pwa/src/components/PermissionDeniedBanner.tsx + modified: + - apps/pwa/src/hooks/usePushSubscription.ts + - apps/pwa/src/components/AppNav.tsx + - apps/pwa/src/components/CalendarShell.tsx + - apps/pwa/src/App.tsx + - apps/pwa/src/styles/tokens.css +decisions: + - "setEnabled(true) + permission=default: no-op; caller must tap-gated subscribe() — iOS user-gesture requirement" + - "readNotificationsDisabled() guards health-check re-subscribe: skip if notificationsEnabled=0 (explicit user off)" + - "persistNotificationsEnabled(false) now writes '0' instead of removing key — allows banner to detect prior-enabled state" + - "onOpenSettings threaded through App → CalendarShell → AppNav (not hoisted to global store) — keeps settings state local to App.tsx" + - "@keyframes spin added to tokens.css — shared by SettingsSheet Loader2 and SyncStateToast spinners" +metrics: + duration: 9 + completed_date: "2026-06-10" + tasks_completed: 3 + files_changed: 7 +--- + +# Phase 05 Plan 08: Opt-Out + Reliability Surface Summary + +Single master notifications toggle (D-09) in an avatar-opened Settings sheet, silent dead-subscription recovery on app open (D-10), and a persistent permission-denied banner for the OS-revoked case (D-10) — completing the user-facing half of the mandatory iOS health-check. + +## Tasks Executed + +### Task 1: usePushSubscription health-check + permission state (D-10) +**Status:** Completed. Commit: `458d6e4` + +Extended `apps/pwa/src/hooks/usePushSubscription.ts`: +- Added `isSubscribed: boolean` state (true when pushManager has active subscription) +- Added `setEnabled(on: boolean)` master toggle: off → unsubscribe + persist '0'; on + permission granted → silent subscribe; on + permission default/denied → no-op +- Health-check now calls `readNotificationsDisabled()` — skips silent re-subscribe if user explicitly turned notifications off (notificationsEnabled=0). Prevents re-subscribing against the user's will. +- Changed `persistNotificationsEnabled(false)` to write '0' instead of removing the key — PermissionDeniedBanner needs to detect "was previously enabled" state +- Exported `readNotificationsEnabled` for PermissionDeniedBanner and SettingsSheet initial-state logic +- Removed dead local usage of `readNotificationsEnabled` (was defined but not in returned interface — the lint hint from the plan) + +### Task 2: SettingsSheet + AppNav avatar button +**Status:** Completed. Commit: `1de4aa5` + +Created `apps/pwa/src/components/SettingsSheet.tsx`: +- Bottom sheet (role="dialog", aria-modal, aria-label="Settings", borderRadius 12px 12px 0 0, zIndex 301, backdrop 300 click-to-close, Escape closes) +- Heading "Settings" + X close button (44px, aria-label="Close settings") +- Section label "NOTIFICATIONS" (uppercase, muted, letter-spacing 0.06em) +- Bell icon + toggle row: "FamilySync Notifications" / "Reminders, event changes, list updates" +- Toggle switch: role="switch", aria-checked, aria-label (on/off variants), 44px touch target + - On: track var(--color-member-0) #4A90D9, thumb white + - Off: track var(--color-border), thumb white + - Disabled (permission denied): opacity 0.5, no pointer events + - Loader2 spinner replaces toggle while subscribing +- Permission-denied hint: AlertCircle + "Notifications are blocked in your browser settings." + "How to enable" link (shown only when permission === 'denied') +- Toggle is wired to `usePushSubscription` — `setEnabled` called on click; initial state from `isSubscribed + permission` + +Modified `apps/pwa/src/components/AppNav.tsx`: +- PhoneNav avatar `div` promoted to `