Linear: https://linear.app/engineereddev/issue/ENG-262
What's in this PR (24 files, all under apps/web/ plus pnpm-lock.yaml):
- Manifest (Pneumo, brand icons incl. maskable, theme/background from tokens,
display: standalone,start_url: /);vite-plugin-pwa2.0.0 in injectManifest mode withsrc/sw.ts; app-shell precache (22 entries) and offline navigation to the shell. sw.ts+ puresw-logic.ts:pushhandler for inlinepushandrefpayloads (token andcontentKeyfrom the IndexedDB key store), §10 notification options (title default, body,tag= push id, brand icon/badge,openlink action), generic "New push" fallback without keys;notificationclickrouting (open→ openWindow; default → focus +show-pushmessage, else/?push=<id>);pushsubscriptionchangere-subscribe + PUT.- Settings "Browser notifications" toggle (subscribe +
PUT /api/devices/self/webpush, unsubscribe + DELETE, denied/unsupported states); sign-out unsubscribes; one-time Home prompt "Get notified in this browser"; "Reload to update" toast for a waiting worker. - Tests: vitest for
sw-logicand the toggle; Playwright for manifest/SW control, a real encrypted payload through the SW push handler with notifications asserted, toggle persistence, the update flow, axe and screenshots.
Verify (lead, rebased on eca40d7):
mise run check:web→ exit 0 (105 tests, build OK,dist/sw.jsgenerated)mise run e2e:web→ exit 0 (27 passed)- Implementer: CDP
Page.getInstallabilityErrors→[](Lighthouse 12 has no PWA category).
Deviations: PWA tests use temporary persistent full-Chromium profiles (headless shell and incognito block the native APIs); synthetic notification clicks stand in for activation-gated focus/open. Real push-service delivery stays the M5 check on the owner's browser.
🤖 Generated with Claude Code