Milestone v1.0: FamilySync MVP #1
@@ -151,6 +151,7 @@ Recent decisions affecting current work:
|
||||
| 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/) |
|
||||
| 260610-k1z | Persist OIDC session cookie (AUTH-02) — @hono/oidc-auth 1.8.3 sets a session-scoped `oidc-auth` cookie (no maxAge) so it died on PWA/browser close → re-login almost every return (both devices). Added persistSessionCookie middleware re-issuing the cookie with maxAge(=OIDC_AUTH_EXPIRES)+SameSite=Lax, ONLY when a valid session exists (no resurrection guard). NOT an Authelia/refresh issue. 14 auth tests pass | 2026-06-10 | 8343fad | Verified | [260610-k1z-persist-oidc-session-cookie-with-maxage-](./quick/260610-k1z-persist-oidc-session-cookie-with-maxage-/) |
|
||||
| 260610-ka9 | Fix silent Android push (Phase 5 UAT Test 4) — SW showNotification had only {body,tag,data} → Android Chromium/Edge showed them silently. Added icon/badge/renotify:true/vibrate; generalized re-enable instructions to Chrome-or-Edge. iOS unaffected. Build emits sw.js with renotify; 187 pwa tests pass | 2026-06-10 | c864fc4 | Verified | [260610-ka9-fix-silent-android-push-notifications-en](./quick/260610-ka9-fix-silent-android-push-notifications-en/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
---
|
||||
phase: 260610-ka9
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/pwa/src/sw.ts
|
||||
- apps/pwa/src/components/InstructionSheet.tsx
|
||||
autonomous: true
|
||||
requirements: [NOTIF-FIX-01, NOTIF-FIX-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Android push notifications alert audibly/visibly (heads-up + sound/vibration), not silently, even when the notification tag is reused."
|
||||
- "iOS push notifications continue to display correctly (added options are ignored by iOS; payload parsing and declarative/legacy logic unchanged)."
|
||||
- "pwa typecheck and build both succeed, emitting dist/sw.js."
|
||||
- "Re-enable instructions name a browser-agnostic Chromium browser (Chrome or Edge), not Chrome only."
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/sw.ts"
|
||||
provides: "Push handler showNotification call enriched with icon, badge, renotify, vibrate"
|
||||
contains: "renotify: true"
|
||||
- path: "apps/pwa/src/components/InstructionSheet.tsx"
|
||||
provides: "Browser-agnostic ANDROID_STEPS first step"
|
||||
contains: "Chrome or Edge"
|
||||
key_links:
|
||||
- from: "apps/pwa/src/sw.ts push handler"
|
||||
to: "self.registration.showNotification options object"
|
||||
via: "NotificationOptions with renotify+vibrate+icon+badge"
|
||||
pattern: "showNotification\\(title"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fix silent Android push notifications. The SW push handler calls `showNotification(title, { body, tag, data })` with minimal options; on Android Chromium (Chrome and Edge) a reused `tag` without `renotify:true` causes the existing notification to be UPDATED silently (no heads-up, sound, or lock-screen alert). Add `icon`, `badge`, `renotify`, and `vibrate` to restore audible/visible alerting. Separately, generalize the Android re-enable copy from Chrome-only to browser-agnostic (Chrome or Edge).
|
||||
|
||||
Purpose: Android UAT confirmed delivery (FCM 201) but silent presentation; this is the user-facing fix. iOS is unaffected and must stay working.
|
||||
Output: Edited `apps/pwa/src/sw.ts` and `apps/pwa/src/components/InstructionSheet.tsx`, with passing typecheck + build.
|
||||
</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/sw.ts
|
||||
@apps/pwa/src/components/InstructionSheet.tsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Enrich showNotification options to fix silent Android push</name>
|
||||
<files>apps/pwa/src/sw.ts</files>
|
||||
<action>
|
||||
In the `push` event handler, modify ONLY the `self.registration.showNotification(title, {...})` options object (the call wrapped in `event.waitUntil(...)`, currently `{ body, tag, data: { url } }`). Add four keys alongside the existing ones, keeping `body`, `tag`, and `data: { url }` exactly as they are:
|
||||
- `icon: '/icon-192.png'` (asset exists at apps/pwa/public/icon-192.png)
|
||||
- `badge: '/icon-192.png'` (asset exists; Android masks it to a monochrome status-bar glyph)
|
||||
- `renotify: true` (re-alert even when the tag is reused — the key fix for silent updates; requires a non-empty tag, which is always set here: default 'familysync-notification' or payload-derived)
|
||||
- `vibrate: [200, 100, 200]` (ensures vibration/heads-up on Android)
|
||||
|
||||
Do NOT set `silent` (it must stay falsy/absent). Do NOT touch the payload parsing, the iOS-18.4+ declarative-vs-legacy branch, the try/catch fallback, `event.waitUntil`, or the `notificationclick` handler. iOS ignores these added options, so its path is unchanged.
|
||||
|
||||
TS caveat: `renotify` and `vibrate` are valid NotificationOptions members but some `lib.dom` TS versions omit/deprecate them on `ServiceWorkerRegistration.showNotification`. If typecheck errors on these keys, resolve it MINIMALLY: declare a `const options: NotificationOptions = { ... }` and pass it (with a narrow `as NotificationOptions` cast only if still required). Do NOT broaden suppression or silence unrelated errors. Build success is the load-bearing gate.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/pwa typecheck && pnpm --filter @familysync/pwa build && test -f apps/pwa/dist/sw.js && grep -q "renotify" apps/pwa/dist/sw.js</automated>
|
||||
</verify>
|
||||
<done>showNotification options include icon, badge, renotify:true, and vibrate; body/tag/data unchanged; `silent` absent; typecheck passes; vite build succeeds and emits apps/pwa/dist/sw.js containing the renotify option.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Make Android re-enable instructions browser-agnostic</name>
|
||||
<files>apps/pwa/src/components/InstructionSheet.tsx</files>
|
||||
<action>
|
||||
In the `ANDROID_STEPS` array, change the first step from `'Open Chrome on your phone'` to a browser-agnostic equivalent that names both Chromium browsers, e.g. `'Open your browser (Chrome or Edge) on your phone'`. Leave the remaining three Android steps unchanged (three-dot menu → Settings → Site Settings → Notifications → Allow — identical across Chromium browsers). Leave `IOS_STEPS` and all component markup/styles untouched. This is a copy-only change; the existing InstructionSheet test does not assert on this wording and must stay green.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "Chrome or Edge" apps/pwa/src/components/InstructionSheet.tsx && pnpm --filter @familysync/pwa test</automated>
|
||||
</verify>
|
||||
<done>ANDROID_STEPS first step names both Chrome and Edge; remaining Android steps and IOS_STEPS unchanged; pwa vitest suite (including InstructionSheet.test.tsx) passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/pwa typecheck` passes (no new TS errors).
|
||||
- `pnpm --filter @familysync/pwa build` (tsc && vite build) succeeds and emits `apps/pwa/dist/sw.js`.
|
||||
- Built `dist/sw.js` contains the `renotify` option (regression guard that the SW edit landed in the bundle).
|
||||
- `pnpm --filter @familysync/pwa test` passes (InstructionSheet test stays green; no sw.ts unit test exists).
|
||||
- Manual/device confirmation (out of plan scope, for UAT): Android push now produces a heads-up + sound/vibration alert; iOS push still displays correctly.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- sw.ts showNotification call includes icon, badge, renotify:true, vibrate; body/tag/data and all parsing/waitUntil logic unchanged; no `silent` key.
|
||||
- InstructionSheet ANDROID_STEPS is browser-agnostic (Chrome or Edge).
|
||||
- typecheck + build + test all pass; dist/sw.js emitted with the fix.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/260610-ka9-fix-silent-android-push-notifications-en/260610-ka9-SUMMARY.md` when done.
|
||||
</output>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
phase: 260610-ka9
|
||||
plan: 01
|
||||
subsystem: pwa/push
|
||||
tags: [push, android, notifications, service-worker, ux]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides: [audible-android-push-notifications]
|
||||
affects: [apps/pwa/src/sw.ts, apps/pwa/src/components/InstructionSheet.tsx]
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns: [NotificationOptions narrow cast for missing lib.dom members]
|
||||
key_files:
|
||||
modified:
|
||||
- apps/pwa/src/sw.ts
|
||||
- apps/pwa/src/components/InstructionSheet.tsx
|
||||
decisions:
|
||||
- "Used `as NotificationOptions` narrow cast (not type suppression) to handle renotify/vibrate absent from this lib.dom; cast is scoped to the single options object"
|
||||
metrics:
|
||||
duration: ~5 min
|
||||
completed: 2026-06-10
|
||||
---
|
||||
|
||||
# Phase 260610-ka9 Plan 01: Fix Silent Android Push Notifications Summary
|
||||
|
||||
**One-liner:** SW showNotification enriched with icon/badge/renotify/vibrate to restore audible Android heads-up alerting; ANDROID_STEPS copy generalized to Chrome or Edge.
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| # | Task | Commit | Files |
|
||||
|---|------|--------|-------|
|
||||
| 1 | Enrich showNotification options to fix silent Android push | c864fc4 | apps/pwa/src/sw.ts |
|
||||
| 2 | Make Android re-enable instructions browser-agnostic | c864fc4 | apps/pwa/src/components/InstructionSheet.tsx |
|
||||
|
||||
## Changes Made
|
||||
|
||||
### apps/pwa/src/sw.ts
|
||||
|
||||
The `showNotification` call previously passed only `{ body, tag, data: { url } }`. On Android Chromium, when the same `tag` is reused the existing notification is silently updated (no heads-up, sound, or vibration). Added four keys to the options object:
|
||||
|
||||
- `icon: '/icon-192.png'` — notification icon (asset exists in public/)
|
||||
- `badge: '/icon-192.png'` — Android status-bar badge (masked to monochrome)
|
||||
- `renotify: true` — forces a re-alert even when tag is reused (the primary fix)
|
||||
- `vibrate: [200, 100, 200]` — ensures vibration/heads-up on Android
|
||||
|
||||
The `body`, `tag`, and `data: { url }` keys are unchanged. `silent` is absent. Payload parsing, the iOS-18.4+ declarative branch, try/catch, and `event.waitUntil` are untouched. iOS ignores the added options.
|
||||
|
||||
**TypeScript note:** `renotify` and `vibrate` are absent from this project's `lib.dom` version of `NotificationOptions`. Resolved with a minimal `as NotificationOptions` cast on the object literal — scoped to these two keys only, no blanket suppression.
|
||||
|
||||
### apps/pwa/src/components/InstructionSheet.tsx
|
||||
|
||||
Changed `ANDROID_STEPS[0]` from `'Open Chrome on your phone'` to `'Open your browser (Chrome or Edge) on your phone'`. All other steps (three-dot menu, Site Settings, Notifications, Allow) are unchanged. IOS_STEPS untouched.
|
||||
|
||||
## Verification Results
|
||||
|
||||
```
|
||||
pnpm --filter @familysync/pwa typecheck
|
||||
→ tsc --noEmit: PASS (no errors)
|
||||
|
||||
pnpm --filter @familysync/pwa build
|
||||
→ tsc && vite build: PASS
|
||||
→ dist/sw.js emitted: YES
|
||||
|
||||
grep -q "renotify" dist/sw.js
|
||||
→ FOUND: renotify in dist/sw.js
|
||||
|
||||
pnpm --filter @familysync/pwa test
|
||||
→ vitest run: 15 test files, 187 tests — all PASSED
|
||||
```
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] TypeScript error: renotify absent from NotificationOptions in this lib.dom**
|
||||
- **Found during:** Task 1 typecheck
|
||||
- **Issue:** `tsc --noEmit` reported `error TS2353: Object literal may only specify known properties, and 'renotify' does not exist in type 'NotificationOptions'` (same for `vibrate`).
|
||||
- **Fix:** Changed `const options: NotificationOptions = { ... }` to `const options = { ... } as NotificationOptions`. The cast is minimal and scoped to the single options object; no other errors suppressed.
|
||||
- **Files modified:** apps/pwa/src/sw.ts
|
||||
- **Commit:** c864fc4
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. Changes are confined to client-side notification display options and UI copy; no new network endpoints, auth paths, or trust boundaries introduced.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `apps/pwa/src/sw.ts` exists and contains `renotify: true` and `as NotificationOptions`
|
||||
- `apps/pwa/src/components/InstructionSheet.tsx` contains `'Open your browser (Chrome or Edge) on your phone'`
|
||||
- Commit c864fc4 exists in git log
|
||||
- `dist/sw.js` emitted and contains `renotify`
|
||||
- All 187 tests pass
|
||||
Reference in New Issue
Block a user