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 @@
-
-
-
-
Thème & couleurs
-
-
-
-
-
- Les couleurs principales de l'app, regroupées. Modifie en direct (aperçu immédiat) puis « Enregistrer ».
- Les éléments qui utilisent les variables --ops-* suivent automatiquement — objectif : réduire et uniformiser la palette.
-
-
-
-
Palettes de départ
-
- Vert accessible + neutres — la palette par défaut, inspirée de la marque
-
-
- Vert lime éclatant (#22c11f) — plus vibrant, contraste texte moindre sur boutons pleins
-
-
-
- Texte principal sur fond de page.
- · texte discret
-
-
-
-
-
- Enregistré dans ce navigateur (aperçu). Pour appliquer à toute l'équipe, on baked la palette choisie dans css/app.scss.
- Les couleurs encore codées en dur dans certaines pages (≈2000) ne suivront qu'après migration vers les variables.
-