ba062e92d0
- App.tsx: wrappers de padding (p-4/md:p-8) e max-w-7xl/mx-auto agora só se aplicam quando não é tela standalone; login/landing/public renderizam full-bleed - OnboardingChat.tsx: classe ob-noscroll esconde a barra de rolagem do slide intro (mantém scroll), substituindo a custom-scrollbar indefinida no fluxo de login Inclui também demais alterações pendentes do working tree (snapshot do estado atual de dev). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Tempo real via WebSocket (singleton, por clínica).
|
|
// O backend empurra { type: 'agenda' | 'notificacao' | 'connected', ... } quando
|
|
// algo muda na clínica ativa → os componentes assinam e refazem o fetch na hora.
|
|
// Reconexão automática com backoff; autentica via token+clinicaId na query string.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
type RTHandler = (msg: any) => void;
|
|
|
|
const handlers = new Set<RTHandler>();
|
|
let ws: WebSocket | null = null;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let backoff = 1000;
|
|
let currentUrl = '';
|
|
|
|
function buildUrl(): string | null {
|
|
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
|
|
let clinicaId = '';
|
|
try { clinicaId = JSON.parse(localStorage.getItem('SCOREODONTO_ACTIVE_WORKSPACE') || '{}')?.id || ''; } catch { /* ignore */ }
|
|
if (!token || !clinicaId) return null;
|
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
return `${proto}://${location.host}/api/ws?token=${encodeURIComponent(token)}&clinicaId=${encodeURIComponent(clinicaId)}`;
|
|
}
|
|
|
|
function scheduleReconnect() {
|
|
if (reconnectTimer || handlers.size === 0) return;
|
|
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, backoff);
|
|
backoff = Math.min(backoff * 2, 15000);
|
|
}
|
|
|
|
function connect() {
|
|
const url = buildUrl();
|
|
if (!url) return;
|
|
currentUrl = url;
|
|
let sock: WebSocket;
|
|
try { sock = new WebSocket(url); } catch { scheduleReconnect(); return; }
|
|
ws = sock;
|
|
sock.onopen = () => { backoff = 1000; };
|
|
sock.onmessage = (ev) => {
|
|
let msg: any;
|
|
try { msg = JSON.parse(ev.data); } catch { return; }
|
|
handlers.forEach(h => { try { h(msg); } catch { /* ignore */ } });
|
|
};
|
|
sock.onclose = () => { if (ws === sock) ws = null; scheduleReconnect(); };
|
|
sock.onerror = () => { try { sock.close(); } catch { /* ignore */ } };
|
|
}
|
|
|
|
function ensureConnected() {
|
|
const url = buildUrl();
|
|
if (!url) return;
|
|
// Clínica/token mudou → reconecta no canal certo.
|
|
if (ws && url !== currentUrl) { try { ws.close(); } catch { /* ignore */ } ws = null; }
|
|
if (!ws || ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) connect();
|
|
}
|
|
|
|
export const Realtime = {
|
|
/** Assina mensagens de tempo real. Retorna função de cancelamento. */
|
|
subscribe(handler: RTHandler): () => void {
|
|
handlers.add(handler);
|
|
ensureConnected();
|
|
return () => {
|
|
handlers.delete(handler);
|
|
if (handlers.size === 0) {
|
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
if (ws) { try { ws.close(); } catch { /* ignore */ } ws = null; }
|
|
}
|
|
};
|
|
},
|
|
/** Força reconexão (ex.: ao trocar de workspace). */
|
|
reconnect() {
|
|
if (ws) { try { ws.close(); } catch { /* ignore */ } ws = null; }
|
|
backoff = 1000;
|
|
ensureConnected();
|
|
},
|
|
};
|