# Shell: Notifications

Source: `apps/oshun/web/src/design-system/components/Toast/`,
`apps/oshun/web/src/components/NotificationsCenterPanel.tsx`,
`apps/oshun/web/src/components/MessageCenter.tsx`, push subscription via
`PwaBootstrap.tsx`/`sw.js`.

Three notification surfaces: ephemeral toasts (right-now feedback), in-app
notifications center (history + unread), and push notifications (off-app
re-engagement).

## Toasts

`ToastProvider` wraps the tree at the root layout level.

- [ ] **Variants** — info, success, warning, error, action (with button)
- [ ] **Position** — bottom-right desktop, bottom-center mobile (above
      bottom-nav)
- [ ] **Stacking** — up to N visible; older toasts collapse or expire
- [ ] **Auto-dismiss** — info/success 4 s, warning 6 s, error 10 s, action
      manual
- [ ] **Manual dismiss** — close button; keyboard `Esc` dismisses focused toast
- [ ] **Action toast** — button performs declared action; auto-dismiss after
      success
- [ ] **Screen reader** — `role="status"` for info/success; `role="alert"` for
      error
- [ ] **Reduced motion** — `prefers-reduced-motion` disables slide animation
- [ ] **Focus return** — focus returns to triggering element when toast
      dismissed
- [ ] **Don't queue infinitely** — if N+ toasts queued, oldest drops

## In-app notifications center

`NotificationsCenterPanel.tsx` — opened from header bell. Customer-message rows
are projected from the customer-message-center store through
`/v1/notifications`; mark-read and dismiss writes flow back through
`/v1/notifications-write` to that same store.

- [x] **Bell** in header — unread count badge (cap at "99+"); customer-message
      count is sourced from the shared inbox snapshot
- [x] **Open** — bell click; Esc closes
- [ ] **Tabs / filters** — all / mentions / system / domain-specific
- [x] **List item** — title, preview, timestamp (relative), read/unread state
      for customer-message notifications
- [x] **Click item** — marks read through the notifications-write route and
      navigates to source for customer-message notifications
- [ ] **Mark all read** — visible action; bulk operation
- [ ] **Empty state** — calm copy, no doom font
- [ ] **Long list** — pagination or virtualization; scroll restored
- [ ] **Realtime** — new notifications appear without reload
      (`OshunQueryProvider` subscription)
- [ ] **Offline** — last-fetched list visible; "you're offline" hint; no broken
      loaders

## Message center

`MessageCenter.tsx` — `/messages` plus a header preview.

- [ ] **`/messages`** — full inbox/outbox view (see
      customer/02-home-discovery/messages.md)
- [ ] **Header preview** — recent N messages; click to open full view
- [ ] **Compose** — destination, subject, body; send via BFF; optimistic update
- [ ] **Delete / archive** — soft-delete with undo toast

## Push notifications

`sw.js` registers `push` and `notificationclick` listeners; subscription managed
in `PwaBootstrap`.

- [ ] **Permission request** — only after a clear user signal (e.g., toggle in
      profile/notifications); never on first page load
- [ ] **Subscription** — `pushManager.subscribe`; endpoint POSTed to BFF
- [ ] **Permission denied** — UI shows "Push is blocked; enable in browser
      settings"
- [ ] **Permission revoked** — detect via `permissionState`; show re-enable hint
- [ ] **Push payload** received →
      `self.registration.showNotification(title, options)`
- [ ] **Notification click** → focuses existing tab if open, else opens
      canonical route
- [ ] **Notification action buttons** (snooze / dismiss / open) trigger correct
      flow
- [ ] **Quiet hours** respected if configured
- [ ] **Unsubscribe** — toggle off in profile; SW unsubscribes; BFF informed

## Notification preferences

Where users configure which channels send which notifications.

- [ ] **Profile → Notifications** — granular per-channel toggles
- [ ] **Categories** — system / domain-specific / mentions / digest
- [ ] **Per-domain** — Tara ritual reminders, Arete habit nudges, Nyx sky-event
      alerts, Veritas story drops, Nisaba study reminders, Metis class deadlines
- [ ] **Quiet hours** — start/end time; timezone-aware
- [ ] **Save** — confirmation toast; settings persist across devices

## Realtime delivery

The notifications center subscribes to a realtime channel (Psyche substrate).
Behavior:

- [ ] **Connected** — new notifications stream in
- [ ] **Disconnected** — reconnect attempted with backoff
- [ ] **Reconnect** — fetch missed notifications via REST snapshot

## States

- [ ] **No notifications** — empty state shown
- [ ] **Some unread** — badge on bell; bold styling on unread items
- [ ] **All read** — no badge; subdued styling
- [ ] **Offline** — last-known state shown; banner hint
- [ ] **Push blocked** — preferences page shows hint to re-enable

## Cross-references

- [01-app-shell.md](./01-app-shell.md) — bell lives in header
- [03-pwa-behavior.md](./03-pwa-behavior.md) — push subscription + SW
- Architecture: [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#iris)
- Feature spec: [`V1/features.md`](../../V1/features.md)

## Open questions / known gaps

- [ ] Confirm whether mentions across domains live in notifications, messages,
      or both
- [ ] Document quiet-hours edge cases (DST, traveler timezone)
- [ ] Email/SMS fall-through channels — do they exist in V1?
