style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -5,21 +5,21 @@ status: human_needed
|
||||
score: 12/12
|
||||
overrides_applied: 0
|
||||
human_verification:
|
||||
- test: "iOS PWA install → push subscription → 15-min reminder receipt"
|
||||
- test: 'iOS PWA install → push subscription → 15-min reminder receipt'
|
||||
expected: "After adding FamilySync to Home Screen on an iOS 16.4+ device and tapping 'Enable Notifications', a push notification appears on the lock screen ~15 minutes before a shared Family-calendar timed event starts."
|
||||
why_human: "iOS-Safari standalone push delivery cannot be driven by playwright-cli per CLAUDE.md — requires a physical iOS device + Home Screen install."
|
||||
- test: "iOS push subscription does not receive NotAllowedError"
|
||||
why_human: 'iOS-Safari standalone push delivery cannot be driven by playwright-cli per CLAUDE.md — requires a physical iOS device + Home Screen install.'
|
||||
- test: 'iOS push subscription does not receive NotAllowedError'
|
||||
expected: "Tapping 'Enable Notifications' on iOS in the installed PWA (or the Settings toggle) successfully calls pushManager.subscribe() without throwing NotAllowedError. Both vapidKey and swRegistration are pre-resolved in state before the tap."
|
||||
why_human: "NEW-CR-01 fix is verified in code (zero awaits between tap and subscribe()), but runtime confirmation on a physical iOS device is the only way to close this."
|
||||
- test: "iOS subscription health-check keeps subscription alive after 1+ week of inactivity"
|
||||
expected: "After a week without opening the app, opening it again silently re-subscribes (if permission still granted) and notifications continue to be delivered."
|
||||
why_human: "Requires real elapsed time and a physical iOS device. Cannot be simulated."
|
||||
- test: "Android FCM: event-change push arrives after the other member modifies a calendar event"
|
||||
why_human: 'NEW-CR-01 fix is verified in code (zero awaits between tap and subscribe()), but runtime confirmation on a physical iOS device is the only way to close this.'
|
||||
- test: 'iOS subscription health-check keeps subscription alive after 1+ week of inactivity'
|
||||
expected: 'After a week without opening the app, opening it again silently re-subscribes (if permission still granted) and notifications continue to be delivered.'
|
||||
why_human: 'Requires real elapsed time and a physical iOS device. Cannot be simulated.'
|
||||
- test: 'Android FCM: event-change push arrives after the other member modifies a calendar event'
|
||||
expected: "When member A modifies a shared event title/time/location, member B receives a push notification on Android within the next 5-minute poll cycle, showing 'A updated an event' with the event title."
|
||||
why_human: "End-to-end push delivery through FCM to a real Android device with a subscribed session cannot be driven by playwright-cli."
|
||||
- test: "List-change push coalescing is observable"
|
||||
expected: "Member B making 5 rapid grocery-list edits results in a SINGLE push notification to member A (not 5), naming the actor and the list, arriving after the 45-second coalesce window."
|
||||
why_human: "Requires two devices/sessions, real timing, and real push delivery. Playwright-cli can exercise the API hooks but not multi-device push receipt."
|
||||
why_human: 'End-to-end push delivery through FCM to a real Android device with a subscribed session cannot be driven by playwright-cli.'
|
||||
- test: 'List-change push coalescing is observable'
|
||||
expected: 'Member B making 5 rapid grocery-list edits results in a SINGLE push notification to member A (not 5), naming the actor and the list, arriving after the 45-second coalesce window.'
|
||||
why_human: 'Requires two devices/sessions, real timing, and real push delivery. Playwright-cli can exercise the API hooks but not multi-device push receipt.'
|
||||
---
|
||||
|
||||
# Phase 5: Web Push Notifications — Verification Report
|
||||
@@ -35,12 +35,12 @@ All four success criteria have substantive, wired, data-flowing server and PWA i
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | Member receives ~15-min push before a shared Family-calendar timed event | VERIFIED (code) / HUMAN (device delivery) | `reminderScheduler.ts`: `runReminderCheck` queries `WHERE isShared=true AND allDay=false AND dtstartUtc BETWEEN now+14m AND now+16m`, fans out via `dispatchPush`; wired in `index.ts` at startup. D-05 enforced in SQL. |
|
||||
| 2 | Other member's event add/change pushes a specific notification | VERIFIED (code) / HUMAN (device delivery) | `eventChangeDispatcher.ts`: `isMeaningfulChange` filters on `dtstartUtc/dtstartDate/allDay/title/location`; `dispatchEventChange` fans out to non-actor subs. `sync.ts` detects diffs and fires `onChanges`; `poller.ts` + `outboxWorker.ts` both pass the callback with `actorUserId`. |
|
||||
| 3 | Other member's shared-list change pushes a generic, coalesced notification | VERIFIED (code) / HUMAN (device delivery) | `listChangeDispatcher.ts`: `notifyListChange` → `coalesceListPush` (45s sliding window, D-01). `lists.ts` calls it on item-add/check/text-edit/delete/list-rename/list-delete; explicitly skipped on position-only PATCH (D-01, line 651). |
|
||||
| 4 | After extended inactivity, push notifications still delivered (health-check) | VERIFIED (code) / HUMAN (device delivery) | `usePushSubscription.ts` mount `useEffect`: checks `getSubscription()`; if missing and not explicitly disabled, silently re-subscribes. D-10. |
|
||||
| # | Truth | Status | Evidence |
|
||||
| --- | ---------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | Member receives ~15-min push before a shared Family-calendar timed event | VERIFIED (code) / HUMAN (device delivery) | `reminderScheduler.ts`: `runReminderCheck` queries `WHERE isShared=true AND allDay=false AND dtstartUtc BETWEEN now+14m AND now+16m`, fans out via `dispatchPush`; wired in `index.ts` at startup. D-05 enforced in SQL. |
|
||||
| 2 | Other member's event add/change pushes a specific notification | VERIFIED (code) / HUMAN (device delivery) | `eventChangeDispatcher.ts`: `isMeaningfulChange` filters on `dtstartUtc/dtstartDate/allDay/title/location`; `dispatchEventChange` fans out to non-actor subs. `sync.ts` detects diffs and fires `onChanges`; `poller.ts` + `outboxWorker.ts` both pass the callback with `actorUserId`. |
|
||||
| 3 | Other member's shared-list change pushes a generic, coalesced notification | VERIFIED (code) / HUMAN (device delivery) | `listChangeDispatcher.ts`: `notifyListChange` → `coalesceListPush` (45s sliding window, D-01). `lists.ts` calls it on item-add/check/text-edit/delete/list-rename/list-delete; explicitly skipped on position-only PATCH (D-01, line 651). |
|
||||
| 4 | After extended inactivity, push notifications still delivered (health-check) | VERIFIED (code) / HUMAN (device delivery) | `usePushSubscription.ts` mount `useEffect`: checks `getSubscription()`; if missing and not explicitly disabled, silently re-subscribes. D-10. |
|
||||
|
||||
**Score:** 12/12 truths verified in codebase.
|
||||
|
||||
@@ -52,78 +52,78 @@ Decision D-05 narrows NOTIF-01 to **shared Family-calendar events only** (person
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `apps/api/src/lib/pushDispatcher.ts` | VAPID send + 410/404 prune | VERIFIED | Exports `dispatchPush` + `buildPushBody`; dual-format payload (web_push:8030 + legacy); 410/404 → `db.delete`; transient → log, no delete. 122 lines. |
|
||||
| `apps/api/src/lib/pushCoalescer.ts` | Per-(list,actor) debounce | VERIFIED | Module-level `pending` Map; sliding setTimeout; exports `coalesceListPush`. 71 lines. |
|
||||
| `apps/api/src/lib/listChangeDispatcher.ts` | Access-scoped, self-suppressed list-change fan-out | VERIFIED | Resolves owner ∪ list_shares audience; excludes actorId; D-02 generic copy `"{actor} made N changes to {list}"`. 127 lines. |
|
||||
| `apps/api/src/lib/eventChangeDispatcher.ts` | Event-change dispatch + isMeaningfulChange | VERIFIED | `MEANINGFUL_FIELDS = {dtstartUtc, dtstartDate, allDay, title, location}`; description-only → silent (D-04); actor excluded via `ne()` + app filter (D-03). Exports `dispatchEventChange` + `isMeaningfulChange`. 176 lines. |
|
||||
| `apps/api/src/broker/reminderScheduler.ts` | 1-min shared-timed-event scan | VERIFIED | `runReminderCheck`: isShared+allDay WHERE in SQL; `sentReminders` dedup Set keyed `uid:minuteBucket`; stale-entry prune (CR-01); `startReminderScheduler` via node-cron. 199 lines. |
|
||||
| `apps/api/src/broker/sync.ts` | title population + onChanges diff callback | VERIFIED | `titleValue` from VEVENT SUMMARY on every upsert; per-uid old-row SELECT; added/updated/deleted classification; `pendingDeleteRows` pre-capture (incl. NEW-WR-01 empty-seenUids branch); `onChanges(changes)` fired at end. |
|
||||
| `apps/api/src/routes/push.ts` | GET /vapid-public-key, POST/DELETE /subscription | VERIFIED | Zod `subscribeSchema`; `resolveUserId` scopes inserts/deletes; upsert on endpoint; 401 when unauthed. |
|
||||
| `apps/api/src/index.ts` | VAPID setup + scheduler wiring | VERIFIED | `webpush.setVapidDetails(...)` in `isMainModule()` guard before `startReminderScheduler()`; `pushRouter` mounted at `/api/push`. |
|
||||
| `apps/pwa/src/sw.ts` | injectManifest SW: precache + push + notificationclick + denylist | VERIFIED | `event.waitUntil(showNotification(...))` always fires (D-11); fallback title/body for malformed payloads; `NavigationRoute` denylist `[/^\/callback/, /^\/api\//, /^\/health/]` (T-03-20); deep-link via `focus()+navigate()` / `openWindow()` (CR-03). |
|
||||
| `apps/pwa/src/hooks/usePushSubscription.ts` | subscribe + health-check + setEnabled | VERIFIED | `subscribe(registration, vapidKey)` — takes pre-resolved reg + key (CR-04/NEW-CR-01); mount health-check (`getSubscription` → silent re-subscribe D-10); `setEnabled` master toggle (D-09). |
|
||||
| `apps/pwa/src/components/PushPermissionPrompt.tsx` | Post-install permission bottom sheet | VERIFIED | Pre-resolves `vapidKey` + `swRegistration` in `useEffect`; button disabled until both ready (NEW-CR-01); `handleEnableClick` calls `subscribe(resolvedRegistration, resolvedVapidKey)` synchronously — zero await before `pushManager.subscribe()`; isInstalled + permission==='default' + !dismissed gate. |
|
||||
| `apps/pwa/src/components/SettingsSheet.tsx` | Master toggle + avatar-triggered sheet | VERIFIED | `role="switch"`, `aria-checked`; 44px target; pre-resolves `vapidKey` + `swRegistration` (CR-04/NEW-CR-01); `handleToggle` calls `subscribe(resolvedRegistration, resolvedVapidKey)` synchronously; permission-denied hint shown inline; Escape closes. |
|
||||
| `apps/pwa/src/components/PermissionDeniedBanner.tsx` | OS-revoked persistent banner | VERIFIED | `role="alert"`; shows only when `permission==='denied' && wasEnabled`; OS-specific instruction sheet (iOS 4-step / Android 4-step); "How to enable" button opens it. |
|
||||
| `apps/api/src/db/schema.ts` | pushSubscriptions table + calendarEvents.title | VERIFIED | `pushSubscriptions` mysqlTable with FK cascade, unique endpoint, userId index. `calendarEvents.title: varchar('title',{length:500})`. |
|
||||
| `apps/api/src/db/migrations/0003_same_xavin.sql` | CREATE TABLE push_subscriptions | VERIFIED | Exists; contains `CREATE TABLE \`push_subscriptions\`` + FK + index. |
|
||||
| `apps/api/src/db/migrations/0004_mature_maximus.sql` | MODIFY COLUMN fixes for endpoint/p256dh lengths | VERIFIED | Contains `MODIFY COLUMN \`endpoint\` varchar(2048)` + `MODIFY COLUMN \`p256dh\` varchar(512)` (CR-02). |
|
||||
| `apps/pwa/vite.config.ts` | injectManifest strategy | VERIFIED | `strategies: 'injectManifest'`; denylist preserved in sw.ts. |
|
||||
| Artifact | Expected | Status | Details |
|
||||
| ---------------------------------------------------- | ----------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `apps/api/src/lib/pushDispatcher.ts` | VAPID send + 410/404 prune | VERIFIED | Exports `dispatchPush` + `buildPushBody`; dual-format payload (web_push:8030 + legacy); 410/404 → `db.delete`; transient → log, no delete. 122 lines. |
|
||||
| `apps/api/src/lib/pushCoalescer.ts` | Per-(list,actor) debounce | VERIFIED | Module-level `pending` Map; sliding setTimeout; exports `coalesceListPush`. 71 lines. |
|
||||
| `apps/api/src/lib/listChangeDispatcher.ts` | Access-scoped, self-suppressed list-change fan-out | VERIFIED | Resolves owner ∪ list_shares audience; excludes actorId; D-02 generic copy `"{actor} made N changes to {list}"`. 127 lines. |
|
||||
| `apps/api/src/lib/eventChangeDispatcher.ts` | Event-change dispatch + isMeaningfulChange | VERIFIED | `MEANINGFUL_FIELDS = {dtstartUtc, dtstartDate, allDay, title, location}`; description-only → silent (D-04); actor excluded via `ne()` + app filter (D-03). Exports `dispatchEventChange` + `isMeaningfulChange`. 176 lines. |
|
||||
| `apps/api/src/broker/reminderScheduler.ts` | 1-min shared-timed-event scan | VERIFIED | `runReminderCheck`: isShared+allDay WHERE in SQL; `sentReminders` dedup Set keyed `uid:minuteBucket`; stale-entry prune (CR-01); `startReminderScheduler` via node-cron. 199 lines. |
|
||||
| `apps/api/src/broker/sync.ts` | title population + onChanges diff callback | VERIFIED | `titleValue` from VEVENT SUMMARY on every upsert; per-uid old-row SELECT; added/updated/deleted classification; `pendingDeleteRows` pre-capture (incl. NEW-WR-01 empty-seenUids branch); `onChanges(changes)` fired at end. |
|
||||
| `apps/api/src/routes/push.ts` | GET /vapid-public-key, POST/DELETE /subscription | VERIFIED | Zod `subscribeSchema`; `resolveUserId` scopes inserts/deletes; upsert on endpoint; 401 when unauthed. |
|
||||
| `apps/api/src/index.ts` | VAPID setup + scheduler wiring | VERIFIED | `webpush.setVapidDetails(...)` in `isMainModule()` guard before `startReminderScheduler()`; `pushRouter` mounted at `/api/push`. |
|
||||
| `apps/pwa/src/sw.ts` | injectManifest SW: precache + push + notificationclick + denylist | VERIFIED | `event.waitUntil(showNotification(...))` always fires (D-11); fallback title/body for malformed payloads; `NavigationRoute` denylist `[/^\/callback/, /^\/api\//, /^\/health/]` (T-03-20); deep-link via `focus()+navigate()` / `openWindow()` (CR-03). |
|
||||
| `apps/pwa/src/hooks/usePushSubscription.ts` | subscribe + health-check + setEnabled | VERIFIED | `subscribe(registration, vapidKey)` — takes pre-resolved reg + key (CR-04/NEW-CR-01); mount health-check (`getSubscription` → silent re-subscribe D-10); `setEnabled` master toggle (D-09). |
|
||||
| `apps/pwa/src/components/PushPermissionPrompt.tsx` | Post-install permission bottom sheet | VERIFIED | Pre-resolves `vapidKey` + `swRegistration` in `useEffect`; button disabled until both ready (NEW-CR-01); `handleEnableClick` calls `subscribe(resolvedRegistration, resolvedVapidKey)` synchronously — zero await before `pushManager.subscribe()`; isInstalled + permission==='default' + !dismissed gate. |
|
||||
| `apps/pwa/src/components/SettingsSheet.tsx` | Master toggle + avatar-triggered sheet | VERIFIED | `role="switch"`, `aria-checked`; 44px target; pre-resolves `vapidKey` + `swRegistration` (CR-04/NEW-CR-01); `handleToggle` calls `subscribe(resolvedRegistration, resolvedVapidKey)` synchronously; permission-denied hint shown inline; Escape closes. |
|
||||
| `apps/pwa/src/components/PermissionDeniedBanner.tsx` | OS-revoked persistent banner | VERIFIED | `role="alert"`; shows only when `permission==='denied' && wasEnabled`; OS-specific instruction sheet (iOS 4-step / Android 4-step); "How to enable" button opens it. |
|
||||
| `apps/api/src/db/schema.ts` | pushSubscriptions table + calendarEvents.title | VERIFIED | `pushSubscriptions` mysqlTable with FK cascade, unique endpoint, userId index. `calendarEvents.title: varchar('title',{length:500})`. |
|
||||
| `apps/api/src/db/migrations/0003_same_xavin.sql` | CREATE TABLE push_subscriptions | VERIFIED | Exists; contains `CREATE TABLE \`push_subscriptions\`` + FK + index. |
|
||||
| `apps/api/src/db/migrations/0004_mature_maximus.sql` | MODIFY COLUMN fixes for endpoint/p256dh lengths | VERIFIED | Contains `MODIFY COLUMN \`endpoint\` varchar(2048)`+`MODIFY COLUMN \`p256dh\` varchar(512)` (CR-02). |
|
||||
| `apps/pwa/vite.config.ts` | injectManifest strategy | VERIFIED | `strategies: 'injectManifest'`; denylist preserved in sw.ts. |
|
||||
|
||||
---
|
||||
|
||||
## Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `poller.ts` | `eventChangeDispatcher.ts` | `onChanges` callback → `dispatchEventChange(change, cred.userId)` | WIRED | `poller.ts:70-79` passes the callback; actor = credential owner. |
|
||||
| `outboxWorker.ts` | `eventChangeDispatcher.ts` | `triggerTargetedResync` → `onChanges` → `dispatchEventChange(change, userId)` | WIRED | `outboxWorker.ts:174-183`; actor = writing member. |
|
||||
| `lists.ts` | `listChangeDispatcher.ts` | `notifyListChange(listId, currentUserId)` at item-add/check/delete/rename/list-delete | WIRED | Lines 388, 437, 505, 652, 706; position-only PATCH guarded at line 651. |
|
||||
| `listChangeDispatcher.ts` | `pushCoalescer.ts` | `coalesceListPush(listId, actorId, dispatch, windowMs)` | WIRED | `listChangeDispatcher.ts:39`. |
|
||||
| `usePushSubscription.ts` | `/api/push/subscription` | `fetch POST sub.toJSON()` inside `subscribe()` | WIRED | `usePushSubscription.ts:225-233`. |
|
||||
| `index.ts` | `webpush.setVapidDetails` | `isMainModule()` guard, before `startReminderScheduler()` | WIRED | `index.ts:120`. |
|
||||
| `sw.ts` | `showNotification` | `event.waitUntil(...)` in push handler | WIRED | `sw.ts:124`. |
|
||||
| `AppNav.tsx` | `SettingsSheet.tsx` | `onOpenSettings` prop → sets `settingsOpen=true` in `App.tsx` | WIRED | `AppNav.tsx:91, 214`; `App.tsx:46,57`. |
|
||||
| `reminderScheduler.ts` | `dispatchPush` | `dispatchPush(sub, notification)` per subscription in event loop | WIRED | `reminderScheduler.ts:147`. |
|
||||
| From | To | Via | Status | Details |
|
||||
| ------------------------- | -------------------------- | ------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------- |
|
||||
| `poller.ts` | `eventChangeDispatcher.ts` | `onChanges` callback → `dispatchEventChange(change, cred.userId)` | WIRED | `poller.ts:70-79` passes the callback; actor = credential owner. |
|
||||
| `outboxWorker.ts` | `eventChangeDispatcher.ts` | `triggerTargetedResync` → `onChanges` → `dispatchEventChange(change, userId)` | WIRED | `outboxWorker.ts:174-183`; actor = writing member. |
|
||||
| `lists.ts` | `listChangeDispatcher.ts` | `notifyListChange(listId, currentUserId)` at item-add/check/delete/rename/list-delete | WIRED | Lines 388, 437, 505, 652, 706; position-only PATCH guarded at line 651. |
|
||||
| `listChangeDispatcher.ts` | `pushCoalescer.ts` | `coalesceListPush(listId, actorId, dispatch, windowMs)` | WIRED | `listChangeDispatcher.ts:39`. |
|
||||
| `usePushSubscription.ts` | `/api/push/subscription` | `fetch POST sub.toJSON()` inside `subscribe()` | WIRED | `usePushSubscription.ts:225-233`. |
|
||||
| `index.ts` | `webpush.setVapidDetails` | `isMainModule()` guard, before `startReminderScheduler()` | WIRED | `index.ts:120`. |
|
||||
| `sw.ts` | `showNotification` | `event.waitUntil(...)` in push handler | WIRED | `sw.ts:124`. |
|
||||
| `AppNav.tsx` | `SettingsSheet.tsx` | `onOpenSettings` prop → sets `settingsOpen=true` in `App.tsx` | WIRED | `AppNav.tsx:91, 214`; `App.tsx:46,57`. |
|
||||
| `reminderScheduler.ts` | `dispatchPush` | `dispatchPush(sub, notification)` per subscription in event loop | WIRED | `reminderScheduler.ts:147`. |
|
||||
|
||||
---
|
||||
|
||||
## Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
|----------|--------------|--------|-------------------|--------|
|
||||
| `reminderScheduler.ts` | `rows` (events × subs) | `db.select().from(calendarEvents).innerJoin(calendars).innerJoin(pushSubscriptions).where(isShared+allDay+window)` | Yes — live DB query | FLOWING |
|
||||
| `eventChangeDispatcher.ts` | `allSubs` (push_subscriptions) | `db.select().from(pushSubscriptions).where(ne(userId, actorId))` | Yes | FLOWING |
|
||||
| `listChangeDispatcher.ts` | `subs` (push_subscriptions for audience) | `db.select().from(pushSubscriptions).where(inArray(userId, audienceIds))` | Yes | FLOWING |
|
||||
| `sync.ts` | `titleValue` | `vevent.getFirstPropertyValue('summary')` from parsed ICAL | Yes — per-sync from VEVENT SUMMARY | FLOWING |
|
||||
| `usePushSubscription.ts` | `isSubscribed` | `registration.pushManager.getSubscription()` (mount health-check) | Yes — live browser Push API | FLOWING |
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
| -------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | ------- |
|
||||
| `reminderScheduler.ts` | `rows` (events × subs) | `db.select().from(calendarEvents).innerJoin(calendars).innerJoin(pushSubscriptions).where(isShared+allDay+window)` | Yes — live DB query | FLOWING |
|
||||
| `eventChangeDispatcher.ts` | `allSubs` (push_subscriptions) | `db.select().from(pushSubscriptions).where(ne(userId, actorId))` | Yes | FLOWING |
|
||||
| `listChangeDispatcher.ts` | `subs` (push_subscriptions for audience) | `db.select().from(pushSubscriptions).where(inArray(userId, audienceIds))` | Yes | FLOWING |
|
||||
| `sync.ts` | `titleValue` | `vevent.getFirstPropertyValue('summary')` from parsed ICAL | Yes — per-sync from VEVENT SUMMARY | FLOWING |
|
||||
| `usePushSubscription.ts` | `isSubscribed` | `registration.pushManager.getSubscription()` (mount health-check) | Yes — live browser Push API | FLOWING |
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| `dispatchPush` deletes on 410 | `vitest run tests/lib/pushDispatcher.test.ts` (test suite known-passing) | Green per orchestrator (213-214 API tests pass) | PASS |
|
||||
| `coalesceListPush` collapses burst | `vitest run tests/lib/pushCoalescer.test.ts` | Green per orchestrator | PASS |
|
||||
| `reminderScheduler` shared/timed filter | `vitest run tests/broker/reminderScheduler.test.ts` | Green per orchestrator | PASS |
|
||||
| `isMeaningfulChange` description-only silent | `vitest run tests/lib/eventChangeDispatcher.test.ts` | Green per orchestrator | PASS |
|
||||
| Push subscription POST/DELETE/vapid-key API | `vitest run tests/routes/push.test.ts` | Green per orchestrator | PASS |
|
||||
| notifyListChange not called on position PATCH | `lists.ts:651` guard verified in code | `if (patch.position === undefined)` before `notifyListChange` | PASS |
|
||||
| PWA builds with sw.js | `pnpm --filter @familysync/pwa build` | Green per orchestrator | PASS |
|
||||
| typecheck passes (api + pwa) | `pnpm --filter @familysync/api typecheck && pnpm --filter @familysync/pwa typecheck` | Green per orchestrator | PASS |
|
||||
| Behavior | Command | Result | Status |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- | ------ |
|
||||
| `dispatchPush` deletes on 410 | `vitest run tests/lib/pushDispatcher.test.ts` (test suite known-passing) | Green per orchestrator (213-214 API tests pass) | PASS |
|
||||
| `coalesceListPush` collapses burst | `vitest run tests/lib/pushCoalescer.test.ts` | Green per orchestrator | PASS |
|
||||
| `reminderScheduler` shared/timed filter | `vitest run tests/broker/reminderScheduler.test.ts` | Green per orchestrator | PASS |
|
||||
| `isMeaningfulChange` description-only silent | `vitest run tests/lib/eventChangeDispatcher.test.ts` | Green per orchestrator | PASS |
|
||||
| Push subscription POST/DELETE/vapid-key API | `vitest run tests/routes/push.test.ts` | Green per orchestrator | PASS |
|
||||
| notifyListChange not called on position PATCH | `lists.ts:651` guard verified in code | `if (patch.position === undefined)` before `notifyListChange` | PASS |
|
||||
| PWA builds with sw.js | `pnpm --filter @familysync/pwa build` | Green per orchestrator | PASS |
|
||||
| typecheck passes (api + pwa) | `pnpm --filter @familysync/api typecheck && pnpm --filter @familysync/pwa typecheck` | Green per orchestrator | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| Requirement | Source Plans | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|----------|
|
||||
| NOTIF-01 | 05-01, 05-06, 05-07 | User receives a Web Push reminder before an event starts | SATISFIED | `reminderScheduler.ts` scans shared timed events in [now+14m, now+16m]; title from `calendarEvents.title` (populated by `sync.ts`); dispatched via `dispatchPush` to all member subscriptions. D-05: shared-calendar-only by design. |
|
||||
| NOTIF-02 | 05-01, 05-03, 05-05 | User receives a Web Push alert when the other member changes a shared list | SATISFIED | `listChangeDispatcher.ts` → `pushCoalescer.ts` → `dispatchPush`; hooked at all meaningful list/item mutations in `lists.ts`; reorder excluded; actor self-suppressed; access-scoped to owner ∪ list_shares. |
|
||||
| NOTIF-03 | 05-01, 05-02, 05-07 | User receives a Web Push alert when an event is added or changed | SATISFIED | `eventChangeDispatcher.ts` (`isMeaningfulChange` + `dispatchEventChange`); `sync.ts` diff + `onChanges`; consumed by `poller.ts` (external changes) + `outboxWorker.ts` (this-member writes); description-only silent (D-04); actor excluded (D-03). |
|
||||
| Requirement | Source Plans | Description | Status | Evidence |
|
||||
| ----------- | ------------------- | -------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| NOTIF-01 | 05-01, 05-06, 05-07 | User receives a Web Push reminder before an event starts | SATISFIED | `reminderScheduler.ts` scans shared timed events in [now+14m, now+16m]; title from `calendarEvents.title` (populated by `sync.ts`); dispatched via `dispatchPush` to all member subscriptions. D-05: shared-calendar-only by design. |
|
||||
| NOTIF-02 | 05-01, 05-03, 05-05 | User receives a Web Push alert when the other member changes a shared list | SATISFIED | `listChangeDispatcher.ts` → `pushCoalescer.ts` → `dispatchPush`; hooked at all meaningful list/item mutations in `lists.ts`; reorder excluded; actor self-suppressed; access-scoped to owner ∪ list_shares. |
|
||||
| NOTIF-03 | 05-01, 05-02, 05-07 | User receives a Web Push alert when an event is added or changed | SATISFIED | `eventChangeDispatcher.ts` (`isMeaningfulChange` + `dispatchEventChange`); `sync.ts` diff + `onChanges`; consumed by `poller.ts` (external changes) + `outboxWorker.ts` (this-member writes); description-only silent (D-04); actor excluded (D-03). |
|
||||
|
||||
All three NOTIF requirements are mapped and implemented. No orphaned requirements for Phase 5.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user