Jason Tudisco 76fcaa1d3c feat(kez-chat/web): always-on inbox stream + unread badge + browser notifications
Previously the SSE stream only ran while the Messages component was
mounted. Navigate to Dashboard or Claims and new messages just piled
up server-side until you came back. Now the stream runs for the whole
session, drives an unread badge in the nav, and (with permission)
fires a system notification when a message lands while you're in
another tab.

inboxService (lib/inbox-service.svelte.ts):
  • Singleton Svelte 5 $state class. session.setUnlocked() starts it,
    session.lock() stops it. Holds the SSE stream + the 30s heartbeat
    poll for the entire session lifetime.
  • Reactive state read by anyone: status (off/connecting/live/
    reconnecting), unreadCount (since last visit to /messages), and
    lastError (surfaced in the Messages footer).
  • onMessage(fn) lets components subscribe to repaint when ingest
    succeeds — Messages page uses this instead of owning its own
    stream.
  • #fireSystemNotification fires Notification API on inbound when
    Notification.permission === "granted" AND document.visibilityState
    !== "visible". Silent while you're actively looking at the tab.
    Uses tag="kez-chat-inbox" so multiple notifications collapse.

Messages.svelte:
  • Stripped its own stream/poll. Now just subscribes to inboxService.
    onMount also calls markAllRead() — landing on /messages = you've
    seen the new stuff.
  • Footer status indicator reads from inboxService instead of local
    state.

App.svelte nav:
  • Messages link grows a red unread-count badge (1, 2, …, 9+) when
    inboxService.unreadCount > 0 and the user isn't already on the
    Messages route.

Dashboard:
  • New "Notifications" section between Quick unlock and Backup with
    the standard 3-state UX: granted (green confirm), denied (amber
    "fix in site settings"), default (button to request).
  • Helpers in inbox-service.ts wrap the Notification API so non-
    supporting browsers (older Safari, Firefox in some configs) get
    graceful "not supported" copy.

Caveat (for v0.3): notifications only fire while the tab is open in
SOME state (background-but-not-closed). Closing the tab kills the
SSE stream so nothing arrives at the page to notify about. True
background push (Web Push API + VAPID + server-side push) is a
separate piece of work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:10:29 -06:00
..