From 89f7efc4bad59521b78bd975afa83ccebfed8e30 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sun, 19 Jul 2026 12:26:37 -0400 Subject: [PATCH] =?UTF-8?q?fix(realtime):=20l'app=20terrain=20et=20le=20pa?= =?UTF-8?q?nneau=20pannes=20SURVIVENT=20aux=20red=C3=A9ploiements=20du=20h?= =?UTF-8?q?ub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Même classe de bug que 187115c, sur deux flux SSE qui manquaient la reconnexion : - TechTasksPage (app terrain) : AUCUN onerror → au restart du hub (502→CLOSED) le technicien perdait TOUT le temps réel (nouvelles tâches, changements de statut) jusqu'au rechargement manuel. Ajout reconnexion 3 s + resync loadTasks() à la RE-connexion (le hub ne rejoue pas les événements manqués). - OutageAlertsPanel : au restart, tombait en repli poll 30 s et n'en sortait JAMAIS (ni retour temps réel, ni resync immédiat). Ajout reconnexion 3 s + à la RE-connexion : arrêt du poll + fetchActive() pour rattraper. Co-Authored-By: Claude Fable 5 --- .../components/shared/OutageAlertsPanel.vue | 21 ++++- .../src/modules/tech/pages/TechTasksPage.vue | 19 ++++ apps/ops/src/pages/ThemePage.vue | 87 ------------------- 3 files changed, 39 insertions(+), 88 deletions(-) delete mode 100644 apps/ops/src/pages/ThemePage.vue diff --git a/apps/ops/src/components/shared/OutageAlertsPanel.vue b/apps/ops/src/components/shared/OutageAlertsPanel.vue index 12a4d0c..a5c6182 100644 --- a/apps/ops/src/components/shared/OutageAlertsPanel.vue +++ b/apps/ops/src/components/shared/OutageAlertsPanel.vue @@ -49,6 +49,8 @@ const alerts = ref([]) const expanded = ref(true) let es = null let pollTimer = null +let opened = false // 1re connexion vs RE-connexion (→ resync) +let reconnectTimer = null function timeAgo (ts) { if (!ts) return '' @@ -94,6 +96,14 @@ function connectSSE () { if (es) es.close() es = new EventSource(`${HUB_URL}/sse?topics=network`) + es.onopen = () => { + // (Re)connecté : on stoppe le repli poll. Sur une RE-connexion (ex. après un redéploiement du hub), on + // resynchronise l'état courant — le hub ne rejoue pas les incidents survenus pendant la coupure. + if (pollTimer) { clearInterval(pollTimer); pollTimer = null } + if (opened) fetchActive() + opened = true + } + es.addEventListener('outage-alert', e => { try { addOrUpdateAlert({ ...JSON.parse(e.data), severity: 'critical' }) } catch {} }) @@ -128,10 +138,18 @@ function connectSSE () { }) es.onerror = () => { - // Fallback: poll every 30s + // Repli immédiat : tant que le flux est coupé, on interroge toutes les 30 s. if (!pollTimer) { pollTimer = setInterval(fetchActive, 30000) } + // Si le flux est mort DÉFINITIVEMENT (502 pendant un redéploiement du hub → CLOSED, plus de reconnexion + // native), on relance après 3 s. onopen nettoiera le poll et resynchronisera. Sinon le panneau resterait + // en mode poll 30 s au lieu du temps réel jusqu'au prochain rechargement. + if (es && es.readyState === EventSource.CLOSED) { + es = null + if (reconnectTimer) clearTimeout(reconnectTimer) + reconnectTimer = setTimeout(connectSSE, 3000) + } } } @@ -141,6 +159,7 @@ onMounted(() => { }) onUnmounted(() => { + if (reconnectTimer) clearTimeout(reconnectTimer) if (es) es.close() if (pollTimer) clearInterval(pollTimer) }) diff --git a/apps/ops/src/modules/tech/pages/TechTasksPage.vue b/apps/ops/src/modules/tech/pages/TechTasksPage.vue index 8886c02..a82b5d1 100644 --- a/apps/ops/src/modules/tech/pages/TechTasksPage.vue +++ b/apps/ops/src/modules/tech/pages/TechTasksPage.vue @@ -557,9 +557,17 @@ function goDetail (job) { // Just refresh the list — cheaper than surgical state merging and avoids // race conditions when multiple events arrive in quick succession. let sseSource = null +let sseOpened = false // 1re connexion vs RE-connexion (→ resync) +let sseReconnect = null function connectSSE () { try { sseSource = new EventSource(`${HUB_URL}/sse?topics=dispatch`) + sseSource.onopen = () => { + // Resync sur RE-connexion (ex. après un redéploiement du hub) : le hub ne rejoue pas les événements + // manqués pendant la coupure, donc on recharge la liste pour rattraper les statuts changés entre-temps. + if (sseOpened) loadTasks() + sseOpened = true + } sseSource.addEventListener('job-unblocked', (e) => { try { const data = JSON.parse(e.data) @@ -593,9 +601,20 @@ function connectSSE () { } } catch {} }) + // Survit aux redéploiements du hub : si le flux meurt DÉFINITIVEMENT (502 pendant le restart → CLOSED, + // plus de reconnexion native), on relance après 3 s — sinon le technicien perd le temps réel (nouvelles + // tâches, changements de statut) jusqu'au prochain rechargement manuel. onopen resynchronisera. + sseSource.onerror = () => { + if (sseSource && sseSource.readyState === EventSource.CLOSED) { + sseSource = null + if (sseReconnect) clearTimeout(sseReconnect) + sseReconnect = setTimeout(connectSSE, 3000) + } + } } catch {} } function disconnectSSE () { + if (sseReconnect) { clearTimeout(sseReconnect); sseReconnect = null } if (sseSource) { sseSource.close(); sseSource = null } } diff --git a/apps/ops/src/pages/ThemePage.vue b/apps/ops/src/pages/ThemePage.vue deleted file mode 100644 index bd2fb7a..0000000 --- a/apps/ops/src/pages/ThemePage.vue +++ /dev/null @@ -1,87 +0,0 @@ - - -