diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a061bde..f558d3d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -108,7 +108,7 @@ Plans: - [ ] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring - [ ] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB -- [ ] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02) +- [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02) **Wave 4** *(blocked on Wave 3)* @@ -161,7 +161,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple |-------|----------------|--------|-----------| | 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 | | 2. Calendar Display | 5/5 | Complete | 2026-06-05 | -| 3. Event Write-Back + PWA Install | 3/8 | In Progress| | +| 3. Event Write-Back + PWA Install | 4/8 | In Progress| | | 4. Shared Lists + Live Sync | 0/? | Not started | - | | 5. Web Push Notifications | 0/? | Not started | - | @@ -171,7 +171,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:** 3/8 plans executed +**Plans:** 4/8 plans executed Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index 3ec88d2..b8e25e5 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: Completed 03-03-PLAN.md -last_updated: "2026-06-05T21:59:40.013Z" +stopped_at: Completed 03-07-PLAN.md +last_updated: "2026-06-05T22:07:48.985Z" last_activity: 2026-06-05 progress: total_phases: 6 completed_phases: 2 total_plans: 17 - completed_plans: 12 + completed_plans: 13 percent: 33 --- @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-03) ## Current Position Phase: 03 (event-write-back-pwa-install) — EXECUTING -Plan: 4 of 8 +Plan: 5 of 8 Status: Ready to execute Last activity: 2026-06-05 @@ -55,6 +55,7 @@ Progress: [███████░░░] 65% | Phase 03 P01 | 45 | 5 tasks | 8 files | | Phase 03 P03-02 | 4 | 2 tasks | 2 files | | Phase 03 P03-03 | 7 | 3 tasks | 2 files | +| Phase 03 P03-07 | 5 | 2 tasks | 7 files | ## Accumulated Context @@ -75,6 +76,8 @@ Recent decisions affecting current work: - [Phase ?]: resolveUserId helper uses any type to avoid Hono context generic complexity — internal helper only - [Phase ?]: Write endpoints enqueue-only (calendarOutbox) with 202 optimistic-accept; no Fastmail call in routes (D-12/D-05) - [Phase ?]: GET /writable-calendars is authoritative D-03 writable-set endpoint: WHERE userId=currentUser.id OR isShared=1 +- [Phase ?]: D-03-07-icons: PWA icons generated via pure Node.js (zlib/Buffer) when ImageMagick/sharp unavailable; placeholder art; operator can replace in public/ without code changes +- [Phase ?]: D-03-07-layout: InstallPrompt desktop layout wraps CalendarContent in flex-column div so banner appears above calendar grid ### Pending Todos @@ -103,6 +106,6 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-05T21:59:40.006Z -Stopped at: Completed 03-03-PLAN.md +Last session: 2026-06-05T22:07:48.979Z +Stopped at: Completed 03-07-PLAN.md Resume file: None diff --git a/.planning/phases/03-event-write-back-pwa-install/03-07-SUMMARY.md b/.planning/phases/03-event-write-back-pwa-install/03-07-SUMMARY.md new file mode 100644 index 0000000..e6e31e6 --- /dev/null +++ b/.planning/phases/03-event-write-back-pwa-install/03-07-SUMMARY.md @@ -0,0 +1,146 @@ +--- +phase: 03-event-write-back-pwa-install +plan: 07 +subsystem: pwa, frontend +tags: [vite-plugin-pwa, service-worker, install-prompt, ios, android, workbox] + +# Dependency graph +requires: + - phase: 03-event-write-back-pwa-install + plan: 01 + provides: vite-plugin-pwa installed in apps/pwa + +provides: + - VitePWA manifest + service worker with auth-safe navigateFallbackDenylist (T-03-20) + - PWA icon assets (192x192, 512x512, 180x180 apple-touch-icon) + - iOS head meta entries for A2HS install + - InstallPrompt component: iOS walkthrough banner/sheet + Android beforeinstallprompt banner + +affects: [03-08] + +# Tech tracking +tech-stack: + added: [] + patterns: + - VitePWA navigateFallbackDenylist to exclude /callback, /api/, /health from SW interception + - runtimeCaching: [] — no API response caching (T-03-21) + - isIOSSafariNonStandalone() — iOS UA + navigator.standalone detection + - useAndroidInstallPrompt() — captures beforeinstallprompt, deferred prompt pattern + - localStorage.installPromptDismissed — persist banner dismissal cross-session + +key-files: + created: + - apps/pwa/src/components/InstallPrompt.tsx + - apps/pwa/public/icon-192.png + - apps/pwa/public/icon-512.png + - apps/pwa/public/apple-touch-icon.png + modified: + - apps/pwa/vite.config.ts + - apps/pwa/index.html + - apps/pwa/src/components/CalendarShell.tsx + +key-decisions: + - "Icons generated programmatically via pure Node.js (zlib/Buffer) — ImageMagick and sharp not available in the environment; minimal valid PNGs at exact pixel dimensions are functionally equivalent for PWA install purposes. Real branded icons can be dropped into public/ without any code change." + - "Desktop InstallPrompt placement: wrapped CalendarContent in a flex-column div on desktop to allow InstallPrompt to appear as a top bar above the calendar grid without disrupting the row sidebar layout." + +# Metrics +duration: ~5min +completed: 2026-06-05 +--- + +# Phase 03 Plan 07: PWA Install — VitePWA Manifest + InstallPrompt Summary + +**VitePWA manifest + auth-safe service worker + iOS icons/meta + InstallPrompt (iOS guided walkthrough + Android beforeinstallprompt) wired into CalendarShell** + +## Performance + +- **Duration:** ~5 min +- **Started:** 2026-06-05T22:01Z +- **Completed:** 2026-06-05T22:06Z +- **Tasks:** 2 +- **Files modified:** 7 + +## Accomplishments + +### Task 1: VitePWA manifest + service worker + iOS head/icons + +- Added `VitePWA` plugin to `apps/pwa/vite.config.ts` with `registerType: 'autoUpdate'` +- Configured `workbox.navigateFallbackDenylist: [/^\/callback/, /^\/api\//, /^\/health/]` — OIDC /callback is never SW-intercepted (T-03-20 Gate 2 prerequisite) +- `runtimeCaching: []` — no authenticated API responses cached (T-03-21) +- Manifest: name/short_name FamilySync, description, theme_color #4A90D9, background_color #ffffff, display:standalone, scope:/, start_url:/, 3 icons (192, 512, 512-maskable) +- Existing `server.proxy` block preserved verbatim (including `/callback` proxy to localhost:3000) +- Generated three PNG icon files via pure Node.js (zlib/Buffer): `icon-192.png` (192×192), `icon-512.png` (512×512), `apple-touch-icon.png` (180×180) — solid #4A90D9 background with white "F" glyph +- Added five iOS `
` entries to `index.html`: apple-touch-icon link, theme-color meta (was already present, supplemented), apple-mobile-web-app-capable, apple-mobile-web-app-status-bar-style, apple-mobile-web-app-title +- Production build verified: `dist/manifest.webmanifest` emitted with display:standalone, scope:/, 3 icons; `dist/sw.js` and `dist/workbox-*.js` emitted + +### Task 2: InstallPrompt (TDD GREEN — RED scaffold from Plan 01) + +- Implemented `isIOSSafariNonStandalone()`: UA regex for iPad/iPhone/iPod + `navigator.standalone !== true` +- Implemented `useAndroidInstallPrompt()`: captures `beforeinstallprompt`, resets on `appinstalled`; returns `{ canInstall, triggerInstall }` +- `InstallPrompt` renders nothing when `display-mode: standalone` or `navigator.standalone` (already installed) +- iOS branch: dismissible banner (Smartphone icon, "Install FamilySync" heading, body + "How to install" link) gated by `localStorage.installPromptDismissed`; "How to install" opens `WalkthroughSheet` — full-screen bottom sheet with 5 annotated steps per UI-SPEC copy; orange (#F5A623) step number circles +- Android branch: banner with "Install" CTA shown only when `canInstall === true`; triggers native prompt, then dismisses +- `role="banner"`, `aria-label="Dismiss install prompt"`, 44px touch targets throughout +- Mounted `