Enhance notification handling to preserve deep-link parameters, add relatedId and relatedType to notifications for better association, and implement Arabic pluralization for meeting reminders.
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.
Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
- VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT
(Docker volume) with /tmp fallback for Replit dev, else ephemeral.
- `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
(orders/meetings/notes), prunes 404/410 endpoints, truncates payload
bodies to ~3500 bytes.
- **De-dup gate:** skips push when the user has any active Socket.IO
connection (uses `io.in(\`user:\${uid}\`).fetchSockets()`), so a
connected user only gets the in-app chime, never a duplicate system
notification.
- `upsertSubscription()` deletes a stale row first when the same
browser endpoint flips to a different user (shared device) so the
previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
`POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into 4 existing emit sites: service-orders `notifyUser`,
notes new-note + reply, executive-meeting broadcast.
Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick only (no
asset caching). All URLs (icon, badge, navigation target) resolved
against `self.registration.scope`, so the SW works at root or under
a subpath without code changes.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status).
- New `PushEnablePrompt` card mounted in `App` — appears on first
launch when supported + permission still "default", one-tap enable,
dismiss persists for 14 days. Silent on unsupported devices.
- `PushToggleRow` added inside Notification Settings.
- ar/en strings: `notifSettings.push.*` and `common.later`.
Plumbing
- OpenAPI: 3 new operations under `notifications`; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.
Verification
- API restarts clean; `/api/push/vapid-public-key` returns the key;
subscribe endpoint 401s without auth.
- New `artifacts/api-server/tests/push.test.mjs` — 7/7 passing —
covers VAPID endpoint, auth gating on subscribe/unsubscribe, row
persistence + removal, idempotent re-subscribe with key rotation,
account-switch endpoint reassignment, and malformed-input rejection.
- Two architect rounds: first flagged race + payload size (fixed),
second flagged dedup gate + first-launch UX + SW base-path + tests
(all fixed in this commit).
Task #554. Adds a full Web Push stack on top of the existing Socket.IO
notification fan-out so the iPad PWA (and any installed browser) receives
system notifications when Tx OS is closed or backgrounded.
Backend
- New `push_subscriptions` table (userId + unique endpoint + p256dh/auth
keys + ua + timestamps), exported from `@workspace/db`.
- `artifacts/api-server/src/lib/push.ts`:
- VAPID bootstrap from env, else cached file at LOCAL_STORAGE_ROOT (Docker
volume) with /tmp fallback for Replit dev, else ephemeral in-memory.
- `sendPushToUser()` honours `notificationsMuted` + per-channel prefs
(orders/meetings/notes), prunes 404/410 endpoints, truncates payload
bodies to ~3500 bytes so over-sized notes don't kill delivery.
- `upsertSubscription()` deletes a stale row first when the same
browser endpoint flips to a different user (account switch on shared
device) so the previous user's notifications can't leak.
- Three new routes: `GET /api/push/vapid-public-key`, `POST /api/push/subscribe`,
`POST /api/push/unsubscribe` (auth-gated, Zod-validated).
- Push hooked into the 4 existing emit sites: service-orders `notifyUser`,
notes new-note + reply, executive-meeting broadcast.
Frontend
- `artifacts/tx-os/public/sw.js`: push + notificationclick handlers only
(no asset caching). Focuses an existing tab or opens a new one at the
payload URL.
- SW registration in `main.tsx` scoped to `BASE_URL`.
- `use-push-subscription` hook (enable/disable/refresh + status:
unsupported / denied / default / subscribed).
- New `PushToggleRow` in `NotificationSettingsContent` with ar/en strings
(notifSettings.push.*). iPad copy explains that the user must add to
Home Screen first for iOS to allow Web Push.
Plumbing
- OpenAPI: 3 new operations under `notifications` tag; orval codegen run.
- `docker-compose.yml` passes VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY /
VAPID_SUBJECT through to the api service.
- `pnpm --filter @workspace/db run push` applied the schema.
- web-push + @types/web-push installed in api-server.
Verification
- API restarts clean; `/api/push/vapid-public-key` returns the generated
key; subscribe endpoint 401s without auth as expected.
- Architect review surfaced two HIGH issues (account-switch leak,
payload size); both fixed before completion.