diff --git a/apps/ops/src/composables/useNotifications.js b/apps/ops/src/composables/useNotifications.js index 12e8120..ab3dd5e 100644 --- a/apps/ops/src/composables/useNotifications.js +++ b/apps/ops/src/composables/useNotifications.js @@ -109,6 +109,9 @@ function onCallIncoming (d) { function initNotifications () { loadPrefs(); loadMyQueues() if (es) return + connectNotifSSE() +} +function connectNotifSSE () { try { es = new EventSource(`${HUB_URL}/sse?topics=outbox,conversations`) es.addEventListener('rating', e => { try { onRating(JSON.parse(e.data)) } catch {} }) @@ -116,6 +119,9 @@ function initNotifications () { es.addEventListener('conv-message', e => { try { onConvMessage(JSON.parse(e.data)) } catch {} }) es.addEventListener('conv-email', e => { try { onConvEmail(JSON.parse(e.data)) } catch {} }) es.addEventListener('call-incoming', e => { try { onCallIncoming(JSON.parse(e.data)) } catch {} }) + // Survit aux redéploiements du hub : si le flux meurt DÉFINITIVEMENT (502 pendant le restart → EventSource CLOSED, + // plus de reconnexion native), on relance après 3 s (sinon la cloche reste muette jusqu'au prochain rechargement). + es.onerror = () => { if (es && es.readyState === EventSource.CLOSED) { es = null; setTimeout(connectNotifSSE, 3000) } } } catch (e) { /* SSE indisponible */ } } function markAllRead () { unread.value = 0 } diff --git a/apps/ops/src/composables/useSSE.js b/apps/ops/src/composables/useSSE.js index 1c2f969..897d23e 100644 --- a/apps/ops/src/composables/useSSE.js +++ b/apps/ops/src/composables/useSSE.js @@ -17,6 +17,7 @@ export function useSSE (opts = {}) { let es = null let reconnectTimer = null let reconnectDelay = 1000 // start at 1s, exponential backoff + let opened = false // pour distinguer 1re connexion vs RECONNEXION (→ resync) function connect (topics) { disconnect() @@ -30,6 +31,10 @@ export function useSSE (opts = {}) { es.onopen = () => { connected.value = true reconnectDelay = 1000 // reset backoff on successful connect + // RESYNC : à toute RE-connexion (après une coupure — ex. redéploiement du hub), on rattrape ce qui a changé pendant + // l'absence en refetchant l'état (le hub ne rejoue pas les deltas manqués). Pas au tout 1er open (le consommateur charge au mount). + if (opened && opts.onReconnect) { try { opts.onReconnect() } catch {} } + opened = true } es.addEventListener('message', (e) => {