Multi-tenant Claude Code monitoring dashboard. FastAPI + PostgreSQL + Docker + SSE real-time updates. Montserrat font, black/#FFC407 color scheme. Apache reverse proxy config at /cc-dashboard/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
61 lines
1.9 KiB
JavaScript
61 lines
1.9 KiB
JavaScript
/**
|
|
* SSE client with auto-reconnect.
|
|
* Heartbeat ": heartbeat" comments keep connection alive through 30s LB timeout.
|
|
*/
|
|
const SSE = (() => {
|
|
const BASE = window.CC_BASE || '';
|
|
let _es = null;
|
|
let _handlers = {};
|
|
let _dotEl = null;
|
|
let _reconnectTimer = null;
|
|
|
|
function setDot(el) { _dotEl = el; }
|
|
|
|
function _setState(state) {
|
|
if (!_dotEl) return;
|
|
_dotEl.className = 'sse-dot ' + state;
|
|
_dotEl.title = state === 'connected' ? 'Live updates active' : state === 'error' ? 'Disconnected — retrying' : 'Connecting…';
|
|
}
|
|
|
|
function on(type, handler) { _handlers[type] = handler; }
|
|
|
|
function connect() {
|
|
if (_es) return;
|
|
_setState('');
|
|
|
|
// EventSource doesn't support custom headers — send token as query param
|
|
const token = Api.getAccessToken();
|
|
// We use a small wrapper: GET /api/events with Bearer via query won't work with
|
|
// standard EventSource. Instead we fetch a one-time SSE ticket or reuse JWT from
|
|
// localStorage. For simplicity, pass token via URL param and validate on server.
|
|
_es = new EventSource(`${BASE}/api/events?token=${encodeURIComponent(token)}`);
|
|
|
|
_es.onopen = () => { _setState('connected'); };
|
|
|
|
_es.onmessage = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
const handler = _handlers[data.type];
|
|
if (handler) handler(data);
|
|
const allHandler = _handlers['*'];
|
|
if (allHandler) allHandler(data);
|
|
} catch { /* ignore parse errors */ }
|
|
};
|
|
|
|
_es.onerror = () => {
|
|
_setState('error');
|
|
_es.close();
|
|
_es = null;
|
|
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
_reconnectTimer = setTimeout(connect, 4000);
|
|
};
|
|
}
|
|
|
|
function disconnect() {
|
|
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
if (_es) { _es.close(); _es = null; }
|
|
_setState('');
|
|
}
|
|
|
|
return { connect, disconnect, on, setDot };
|
|
})();
|