docs(quick-260610-jlp): plan/summary + STATE row (how-to-enable fix, Verified)
This commit is contained in:
@@ -149,6 +149,7 @@ Recent decisions affecting current work:
|
||||
| 260610-czd | Fix docs/deployment.md local-dev command — added "Running locally (host-side, no Docker)" subsection with correct env-sourced two-terminal run command (Phase 2 UAT gap) | 2026-06-10 | 39e2ee0 | | [260610-czd-fix-docs-deployment-md-local-dev-command](./quick/260610-czd-fix-docs-deployment-md-local-dev-command/) |
|
||||
| 260610-hbu | Phase 5 reminder scheduler resilience (UAT Test 1 gap) — catch-up scan `(now, now+16min]` + per-uid exactly-once dedup so a missed/late cron tick no longer drops a reminder; lead-accurate body; also fixes pre-existing cross-tick double-fire. 10/10 reminder tests pass | 2026-06-10 | 19d92c6 | Verified | [260610-hbu-make-phase-5-reminder-scheduler-resilien](./quick/260610-hbu-make-phase-5-reminder-scheduler-resilien/) |
|
||||
| 260610-i4x | Replace node-cron with setInterval in all 3 broker workers (poller/outbox/reminder) — node-cron 4.2.1 skipped EVERY scheduled execution in the long-running API process ("missed execution" each tick), so reminders/poll/outbox never fired on schedule. setInterval fires reliably (verified). 91 broker tests pass | 2026-06-10 | d9efbc1 | Verified | [260610-i4x-replace-node-cron-with-setinterval-in-ba](./quick/260610-i4x-replace-node-cron-with-setinterval-in-ba/) |
|
||||
| 260610-jlp | Fix broken "How to enable" link in notifications-blocked UI (Phase 5 UAT Test 4) — extracted InstructionSheet into a shared component; SettingsSheet "How to enable" now opens the OS-step instructions instead of just closing the sheet. 187 pwa tests pass, build green | 2026-06-10 | f82837c | Verified | [260610-jlp-fix-broken-how-to-enable-link-in-notific](./quick/260610-jlp-fix-broken-how-to-enable-link-in-notific/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
---
|
||||
phase: quick-260610-jlp
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/pwa/src/components/InstructionSheet.tsx
|
||||
- apps/pwa/src/components/PermissionDeniedBanner.tsx
|
||||
- apps/pwa/src/components/SettingsSheet.tsx
|
||||
- apps/pwa/src/components/InstructionSheet.test.tsx
|
||||
autonomous: true
|
||||
requirements: [UAT-05-T4]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Tapping 'How to enable' in SettingsSheet's permission-denied hint opens the OS-specific instruction dialog (role=\"dialog\") instead of closing the sheet"
|
||||
- "PermissionDeniedBanner still opens its instruction sheet identically after the extraction (behaviour-preserving)"
|
||||
- "The instruction sheet copy/markup/styles are unchanged from the original local InstructionSheet"
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/components/InstructionSheet.tsx"
|
||||
provides: "Shared InstructionSheet bottom-sheet dialog + isIOS/IOS_STEPS/ANDROID_STEPS, named export"
|
||||
contains: "export function InstructionSheet"
|
||||
- path: "apps/pwa/src/components/SettingsSheet.tsx"
|
||||
provides: "Permission-denied hint wired to open InstructionSheet"
|
||||
contains: "setInstructionsOpen"
|
||||
- path: "apps/pwa/src/components/PermissionDeniedBanner.tsx"
|
||||
provides: "Banner importing the shared InstructionSheet (local copy removed)"
|
||||
contains: "import { InstructionSheet }"
|
||||
- path: "apps/pwa/src/components/InstructionSheet.test.tsx"
|
||||
provides: "Test asserting SettingsSheet 'How to enable' opens role=dialog rather than calling onClose"
|
||||
key_links:
|
||||
- from: "apps/pwa/src/components/SettingsSheet.tsx"
|
||||
to: "apps/pwa/src/components/InstructionSheet.tsx"
|
||||
via: "named import + instructionsOpen state render"
|
||||
pattern: "import \\{ InstructionSheet \\}.*InstructionSheet\\.js"
|
||||
- from: "apps/pwa/src/components/PermissionDeniedBanner.tsx"
|
||||
to: "apps/pwa/src/components/InstructionSheet.tsx"
|
||||
via: "named import"
|
||||
pattern: "import \\{ InstructionSheet \\}.*InstructionSheet\\.js"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fix the broken "How to enable" link in SettingsSheet's notifications-blocked hint (Phase 5 UAT Test 4). On Android the link did nothing except close the settings sheet — `onClick={onClose}` showed no guidance. Extract the already-working `InstructionSheet` from PermissionDeniedBanner into a shared component and wire SettingsSheet's hint to open it.
|
||||
|
||||
Purpose: A browser-blocked user cannot re-enable notifications from inside the app (correct), so the ONLY recovery is the OS-specific step-by-step guidance. That guidance was unreachable from Settings — the documented recovery path was dead.
|
||||
|
||||
Output: New shared `InstructionSheet.tsx`; PermissionDeniedBanner refactored to import it (behaviour identical); SettingsSheet wired to open it; a focused test proving the wiring.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@CLAUDE.md
|
||||
@apps/pwa/src/components/PermissionDeniedBanner.tsx
|
||||
@apps/pwa/src/components/SettingsSheet.tsx
|
||||
@apps/pwa/src/components/InstallPrompt.test.tsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Extract InstructionSheet into a shared component and rewire PermissionDeniedBanner</name>
|
||||
<files>apps/pwa/src/components/InstructionSheet.tsx, apps/pwa/src/components/PermissionDeniedBanner.tsx</files>
|
||||
<action>
|
||||
Create `apps/pwa/src/components/InstructionSheet.tsx`. Move VERBATIM from PermissionDeniedBanner.tsx (lines ~25-202): the `isIOS()` function, the `IOS_STEPS` and `ANDROID_STEPS` constants, the `InstructionSheetProps` interface, and the `InstructionSheet` component. Keep markup, inline styles, CSS-var design tokens, role="dialog"/aria-modal/aria-label, the X close button, the numbered steps, and the Done button BYTE-FOR-BYTE identical — this is a behaviour-preserving extraction, NOT a redesign (UI-SPEC §Surface; T-05-24: copy stays plain-text JSX children, no dangerouslySetInnerHTML). Change `function InstructionSheet` to `export function InstructionSheet` (named export). Carry the `import { X } from 'lucide-react'` into the new file (only X is needed there). `isIOS()` MUST move with the component since it selects the step list + platform label.
|
||||
|
||||
Edit `apps/pwa/src/components/PermissionDeniedBanner.tsx`: delete the now-moved `isIOS()`, `IOS_STEPS`, `ANDROID_STEPS`, `InstructionSheetProps`, and local `InstructionSheet` (the "OS detection", "Instruction steps", and "Instruction sheet" sections). Add `import { InstructionSheet } from './InstructionSheet.js'` (match the existing `.js`-specifier convention used by `usePushSubscription.js`). The remaining import from `lucide-react` keeps `AlertCircle`; drop `X` from that import ONLY if no longer used in this file (grep confirms — X was only used inside the moved InstructionSheet). Leave the `PermissionDeniedBanner` function body unchanged — it already references `InstructionSheet` and `setInstructionsOpen`, now satisfied by the import. Do NOT touch the banner's render/visibility logic.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && grep -q "export function InstructionSheet" src/components/InstructionSheet.tsx && grep -q "import { InstructionSheet } from './InstructionSheet.js'" src/components/PermissionDeniedBanner.tsx && ! grep -q "function InstructionSheet" src/components/PermissionDeniedBanner.tsx && pnpm --filter @familysync/pwa typecheck</automated>
|
||||
</verify>
|
||||
<done>InstructionSheet.tsx exists with a named `InstructionSheet` export plus isIOS/IOS_STEPS/ANDROID_STEPS; PermissionDeniedBanner imports it and no longer defines a local InstructionSheet; `pnpm --filter @familysync/pwa typecheck` exits 0.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wire SettingsSheet's "How to enable" to the shared InstructionSheet (with test)</name>
|
||||
<files>apps/pwa/src/components/SettingsSheet.tsx, apps/pwa/src/components/InstructionSheet.test.tsx</files>
|
||||
<behavior>
|
||||
- When SettingsSheet renders with permission==='denied' and the user clicks "How to enable", a role="dialog" element appears (the InstructionSheet) and onClose (the sheet-close prop) is NOT called.
|
||||
- The shared InstructionSheet renders role="dialog" with the heading "How to enable notifications" and a "Done" button that calls its onClose.
|
||||
</behavior>
|
||||
<action>
|
||||
Edit `apps/pwa/src/components/SettingsSheet.tsx`: `useState` is already imported (line 24) — no import change needed there. Add `import { InstructionSheet } from './InstructionSheet.js'` alongside the other component imports. Inside the `SettingsSheet` function, add `const [instructionsOpen, setInstructionsOpen] = useState(false)` near the other useState hooks. Change the broken "How to enable" button (currently `onClick={onClose}`, ~line 373) to `onClick={() => setInstructionsOpen(true)}`. At the end of the returned fragment (after the closing `</div>` of the Sheet, before the fragment's closing `</>`), render `{instructionsOpen && (<InstructionSheet onClose={() => setInstructionsOpen(false)} />)}`. The InstructionSheet is `position: fixed; zIndex: 1000` so it correctly layers above the settings sheet (zIndex 301). Do NOT change the disabled-toggle behaviour or any other logic — only the guidance link was broken; staying unable to re-enable in-app while browser-blocked is correct.
|
||||
|
||||
Create `apps/pwa/src/components/InstructionSheet.test.tsx` following the existing harness style (vitest + @testing-library/react + `.js` import specifiers, see InstallPrompt.test.tsx). Write the test FIRST and watch it fail against the un-wired SettingsSheet (RED), then make it pass (GREEN). Mock the `usePushSubscription` hook (`vi.mock('../hooks/usePushSubscription.js', ...)`) to return `{ permission: 'denied', isSubscribed: false, subscribe: vi.fn(), setEnabled: vi.fn() }` so the permission-denied hint renders. Test: render `<SettingsSheet isOpen={true} onClose={onCloseSpy} />`, assert no `role="dialog"` named "How to enable notifications" exists yet, click the "How to enable" button (`screen.getByText('How to enable')`), then assert (a) a dialog with the "How to enable notifications" heading is now visible and (b) `onCloseSpy` was NOT called. Keep it pragmatic — if jsdom lacks `Notification`, define a minimal `globalThis.Notification = { permission: 'denied' }` stub in the test before render. Do not over-engineer; one wiring assertion is sufficient.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && grep -q "onClick={() => setInstructionsOpen(true)}" src/components/SettingsSheet.tsx && grep -q "instructionsOpen && " src/components/SettingsSheet.tsx && ! grep -q "onClick={onClose}\s*$" <(grep -A1 "How to enable" src/components/SettingsSheet.tsx) ; pnpm --filter @familysync/pwa test -- src/components/InstructionSheet.test.tsx</automated>
|
||||
</verify>
|
||||
<done>SettingsSheet's "How to enable" opens the InstructionSheet via `instructionsOpen` state (no longer calls onClose); InstructionSheet.test.tsx passes asserting the dialog opens and onClose is not called.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Full typecheck, build, and test gate</name>
|
||||
<files>apps/pwa/src/components/InstructionSheet.tsx, apps/pwa/src/components/PermissionDeniedBanner.tsx, apps/pwa/src/components/SettingsSheet.tsx</files>
|
||||
<action>
|
||||
Run the full PWA quality gate to prove the extraction + wiring compiles, builds, and all tests pass (regression guard — confirms PermissionDeniedBanner still typechecks against the shared import and no other consumer broke). If any check fails, fix the cause in the files above (do not weaken or skip checks). No new dependencies; lucide-react and the CSS-var tokens already in use stay as-is.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/pwa && pnpm --filter @familysync/pwa typecheck && pnpm --filter @familysync/pwa build && pnpm --filter @familysync/pwa test</automated>
|
||||
</verify>
|
||||
<done>`typecheck` exits 0; `build` (tsc && vite build) succeeds; full `test` suite passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/pwa typecheck` exits 0
|
||||
- `pnpm --filter @familysync/pwa build` succeeds (tsc && vite build)
|
||||
- `pnpm --filter @familysync/pwa test` passes (including the new InstructionSheet.test.tsx)
|
||||
- `grep` confirms SettingsSheet's "How to enable" calls `setInstructionsOpen(true)`, not `onClose`
|
||||
- `grep` confirms PermissionDeniedBanner imports InstructionSheet and no longer defines it locally
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- A browser-blocked user opening Settings and tapping "How to enable" sees the OS-specific instruction dialog (the original UAT-05-T4 failure is fixed).
|
||||
- PermissionDeniedBanner behaves identically to before (behaviour-preserving extraction).
|
||||
- InstructionSheet markup/copy/styles unchanged; no new dependencies; no dangerouslySetInnerHTML.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/260610-jlp-fix-broken-how-to-enable-link-in-notific/260610-jlp-SUMMARY.md` when done
|
||||
</output>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
---
|
||||
phase: quick-260610-jlp
|
||||
plan: 01
|
||||
subsystem: pwa/notifications
|
||||
tags: [bug-fix, refactor, uat, push-notifications]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides: [InstructionSheet shared component, SettingsSheet how-to-enable wired]
|
||||
affects: [PermissionDeniedBanner, SettingsSheet]
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns: [shared component extraction, local state for sheet overlay]
|
||||
key_files:
|
||||
created:
|
||||
- apps/pwa/src/components/InstructionSheet.tsx
|
||||
- apps/pwa/src/components/InstructionSheet.test.tsx
|
||||
modified:
|
||||
- apps/pwa/src/components/PermissionDeniedBanner.tsx
|
||||
- apps/pwa/src/components/SettingsSheet.tsx
|
||||
decisions:
|
||||
- "Extracted InstructionSheet verbatim from PermissionDeniedBanner; zero markup/copy/style changes (behaviour-preserving extraction)"
|
||||
- "SettingsSheet uses local instructionsOpen state to gate rendering InstructionSheet, not onClose"
|
||||
metrics:
|
||||
duration: ~8 minutes
|
||||
completed: "2026-06-10"
|
||||
requirements: [UAT-05-T4]
|
||||
---
|
||||
|
||||
# Phase quick-260610-jlp Plan 01: Fix Broken "How to Enable" Link in SettingsSheet
|
||||
|
||||
**One-liner:** Extracted shared InstructionSheet bottom-sheet from PermissionDeniedBanner and wired SettingsSheet's "How to enable" button to open it via local state (was: `onClick={onClose}` — dead link).
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| Task | Name | Commit | Files |
|
||||
|------|------|--------|-------|
|
||||
| 1 | Extract InstructionSheet into shared component + rewire PermissionDeniedBanner | 74b5d44 | InstructionSheet.tsx (new), PermissionDeniedBanner.tsx |
|
||||
| 2 | Wire SettingsSheet "How to enable" + write test | 874c030 | SettingsSheet.tsx, InstructionSheet.test.tsx (new) |
|
||||
| 3 | Full typecheck + build + test gate | (no code change) | — |
|
||||
|
||||
## Quality Gate Results
|
||||
|
||||
- `pnpm --filter @familysync/pwa typecheck`: exit 0 (no output)
|
||||
- `pnpm --filter @familysync/pwa build`: SUCCESS — 1851 modules transformed, dist/assets/index-CaPAI7hF.js 655.55 kB gzip 191.46 kB, dist/sw.js 17.09 kB
|
||||
- `pnpm --filter @familysync/pwa test`: **15 test files passed, 187 tests passed (0 failed)**
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed exactly as written.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — all wiring is functional; InstructionSheet renders real steps.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None — no new network endpoints, auth paths, or trust boundary changes. The InstructionSheet is a local display component.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `apps/pwa/src/components/InstructionSheet.tsx` — exists, named export confirmed
|
||||
- `apps/pwa/src/components/InstructionSheet.test.tsx` — exists, 2 new tests pass
|
||||
- `apps/pwa/src/components/PermissionDeniedBanner.tsx` — imports from InstructionSheet.js, no local definition
|
||||
- `apps/pwa/src/components/SettingsSheet.tsx` — `setInstructionsOpen(true)` on "How to enable", `instructionsOpen &&` render
|
||||
- Commits 74b5d44 and 874c030 both exist in git log
|
||||
Reference in New Issue
Block a user