One Channel Plan — RIGHT

Date: 2026-04-06 Status: TO DO

Goal

ONE real-time channel: SSE. Both dashboard and chat read everything from SSE. Per-bot WS is on-demand request-response only (chat.send, chat.history). dash-ws goes away as a real-time event channel.

Why SSE

Changes

1. Dashboard socket.ts — replace WebSocket with EventSource

Current: connects to /ws/dash (WebSocket), dispatches {event, data} messages to listeners. New: connects to /api/events (SSE), dispatches SSE events to the same on/off listener API.

Zero component changes. The useSocketEvent hook and every socket.on() call works unchanged. Only the transport layer inside socket.ts changes.

SSE events arrive as JSON with a type field. The new socket.ts maps event.type to the listener name: {type: 'bot-activity', botId: '...', ...}emit('bot-activity', event).

2. Server /api/events — fix dashboard subscription

Current:

const sseKey = (originHost === config.chatDomain) ? '*' : originHost;
// Dashboard gets sseKey = 'dash.edgebot.app' → matches NO botId → gets NO events

New:

const sseKey = '*';  // All authenticated clients get all events (filtered by access client-side)

Or better: pass accessible botIds and subscribe to each. But * is simpler and the dashboard already has access control via the API. Events are cheap; rendering is gated.

3. Route dash-ws-only events through SSE

Every event that currently ONLY goes through broadcast() / broadcastToBot() / _ioProxy.emit() needs to also go through pushBotEvent().

Event Current source Change
bot-activity _ioProxy.emit in broadcastBotStatus (ws-manager.ts) Add pushBotEvent(botId, {type: 'bot-activity', ...})
bots-status-update _ioProxy.emit in broadcastBotStatus (ws-manager.ts) Add pushBotEvent(botId, {type: 'bots-status-update', ...})
auth-profiles:refreshed broadcast() in token-refresh.ts Replace with pushBotEvent('*', {type: 'auth-profiles:refreshed', ...})
auth-profiles:usage-updated broadcast() in usage-cache.ts (2 places) Replace with pushBotEvent('*', {type: 'auth-profiles:usage-updated', ...})
code-session:status broadcastToBot() in code-session.ts Replace with pushBotEvent(botId, {type: 'code-session:status', ...})
code-session:event broadcastToBot() in code-session.ts Replace with pushBotEvent(botId, {type: 'code-session:event', ...})
docker:status broadcast() in docker-env.ts (~10 places) Replace with pushBotEvent('*', {type: 'docker:status', ...})
chat-message dashIo.emit() in message-capture.ts Replace with pushBotEvent(botId, {type: 'chat-message', ...})
provision-log io.emit() in bot-provision.ts Replace with pushBotEvent('*', {type: 'provision-log', ...})
provision-done io.emit() in bot-provision.ts Replace with pushBotEvent('*', {type: 'provision-done', ...})

System-wide events (no specific bot) use pushBotEvent('*', ...) which reaches all * subscribers.

4. Remove Phase 1 broadcastToBot calls from emitBotEvent

The broadcastToBot('chat-delta', ...) and broadcastToBot('chat-status', ...) calls added in Phase 1 are now redundant — SSE already delivers these events. Remove them.

5. Remove dash-ws listeners from dashboard use-chat.ts

The chat-delta, chat-status, and chat-message dash-ws listeners in use-chat.ts are replaced by SSE events. Remove them and add SSE-based listeners instead (or handle in app-shell.tsx).

6. Remove broadcastToAllWebchat calls

broadcastToAllWebchat in usage-cache.ts and token-refresh.ts sends to per-bot WS connections. Per-bot WS is request-response only now. These calls become pushBotEvent('*', ...) instead.

7. Clean up broadcastBotStatus

Currently iterates webchatConnections directly to send bot:status to per-bot WS clients. Remove that — bot status goes through SSE only.

8. Stop dashboard from connecting to dash-ws

After all events flow through SSE, the dashboard no longer needs /ws/dash. Remove the WebSocket connection entirely (already done by replacing socket.ts in step 1).

dash-ws.ts can remain on the server for now (other things may reference it) but nothing connects to it.

Files touched

File Change
clients/dashboard/src/lib/socket.ts WebSocket → EventSource
routes/api.ts Fix SSE subscription key for dashboard
lib/ws-manager.ts Push bot-activity/bots-status-update to SSE, remove broadcastToBot calls, clean up broadcastBotStatus
lib/usage-cache.ts Replace broadcast/broadcastToAllWebchat with pushBotEvent
lib/jobs/token-refresh.ts Replace broadcast/broadcastToAllWebchat with pushBotEvent
lib/code-session.ts Replace broadcastToBot with pushBotEvent
lib/message-capture.ts Replace dashIo.emit with pushBotEvent
api/docker-env.ts Replace broadcast with pushBotEvent
api/bot-provision.ts Replace io.emit with pushBotEvent
clients/dashboard/src/hooks/use-chat.ts Remove dash-ws listeners, add SSE-based streaming

End state

Client Channel Send
Dashboard SSE (all events) HTTP API
Chat SSE (all events) HTTP API (primary), per-bot WS fallback
Per-bot WS Request-response only chat.send, chat.history