diff --git a/web/static/css/custom.css b/web/static/css/custom.css index f6385133..6aed4c91 100644 --- a/web/static/css/custom.css +++ b/web/static/css/custom.css @@ -452,6 +452,7 @@ footer { .di-pill--completed { --accent: #4CD964; --accent-dim: rgba(76,217,100,0.2); --accent-glow: rgba(76,217,100,0.15); } .di-pill--completed-errors { --accent: #FF9500; --accent-dim: rgba(255,149,0,0.2); --accent-glow: rgba(255,149,0,0.15); } .di-pill--idle { --accent: #e5a00d; --accent-dim: rgba(229,160,13,0.15); --accent-glow: rgba(229,160,13,0.1); } +.di-pill--disconnected { --accent: #FF3B30; --accent-dim: rgba(255,59,48,0.2); --accent-glow: rgba(255,59,48,0.15); } /* Slide-down entrance */ @keyframes diSlideDown { @@ -585,6 +586,7 @@ footer { /* Error — red glow border */ .di-pill--error { border: 1.5px solid rgba(255,59,48,0.3); } +.di-pill--disconnected { border: 1.5px solid rgba(255,59,48,0.3); } /* Running pills use a fixed width in compact mode to prevent jumpiness as content changes (phase labels, counts, durations) every 2s poll */ diff --git a/web/templates/components/global_operation_banner.html b/web/templates/components/global_operation_banner.html index d976e29f..bb51fd04 100644 --- a/web/templates/components/global_operation_banner.html +++ b/web/templates/components/global_operation_banner.html @@ -305,9 +305,66 @@ } }); +// ── Connection-lost detection ───────────────────────────────────────── +// Track consecutive banner poll failures. After 3 failures, replace the +// pill with a "Connection Lost" state so the user knows the server is +// unreachable (instead of the banner just freezing on its last state). +var _diBannerFailCount = 0; +var _diBannerDisconnected = false; + +document.addEventListener('htmx:sendError', function(evt) { + if (!evt.detail.elt || !evt.detail.elt.closest('#global-operation-banner')) return; + _diBannerFailCount++; + if (_diBannerFailCount >= 3 && !_diBannerDisconnected) { + _diBannerDisconnected = true; + var banner = document.getElementById('global-operation-banner'); + if (!banner) return; + // Preserve expansion state on outer div — only replace inner content + banner.innerHTML = + '