From 76fcaa1d3c6a4892a332700ff363b2cd3414df3e Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Wed, 27 May 2026 00:10:29 -0600 Subject: [PATCH] feat(kez-chat/web): always-on inbox stream + unread badge + browser notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- kez-chat/web/src/App.svelte | 13 +- kez-chat/web/src/lib/inbox-service.svelte.ts | 203 +++++++++++++++++++ kez-chat/web/src/lib/store.svelte.ts | 7 + kez-chat/web/src/routes/Dashboard.svelte | 47 +++++ kez-chat/web/src/routes/Messages.svelte | 121 ++--------- 5 files changed, 291 insertions(+), 100 deletions(-) create mode 100644 kez-chat/web/src/lib/inbox-service.svelte.ts diff --git a/kez-chat/web/src/App.svelte b/kez-chat/web/src/App.svelte index 10f35ba..0445c4c 100644 --- a/kez-chat/web/src/App.svelte +++ b/kez-chat/web/src/App.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import { hasStoredIdentity } from "./lib/identity-store.js"; import { session } from "./lib/store.svelte.js"; + import { inboxService } from "./lib/inbox-service.svelte.js"; import Landing from "./routes/Landing.svelte"; import CreateAccount from "./routes/CreateAccount.svelte"; @@ -46,7 +47,17 @@ {#if session.unlocked}