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
- Redis-buffered with replay on reconnect (Last-Event-ID)
- Works through Caddy (confirmed)
- Already carries the most events (chat deltas, new_message, tool, queue, etc.)
- Unidirectional (server → client) — dashboard is receive-only, perfect fit
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 |