Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web/static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 */
Expand Down
57 changes: 57 additions & 0 deletions web/templates/components/global_operation_banner.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'<div class="di-pill di-pill--disconnected" data-state="disconnected"' +
' hx-get="/api/operation-banner" hx-trigger="every 5s" hx-swap="innerHTML" hx-target="#global-operation-banner">' +
' <div class="di-pill__compact">' +
' <div class="di-pill__left">' +
' <div class="di-icon"><i data-lucide="wifi-off" style="width:14px;height:14px;"></i></div>' +
' <span class="di-label">Connection Lost</span>' +
' </div>' +
' </div>' +
' <div class="di-pill__expanded">' +
' <div class="di-expanded__header">' +
' <div class="di-expanded__header-left">' +
' <div class="di-icon"><i data-lucide="wifi-off" style="width:14px;height:14px;"></i></div>' +
' <span class="di-label">Server Unreachable</span>' +
' </div>' +
' </div>' +
' <div style="font-size:0.78rem;color:rgba(255,255,255,0.5);text-align:center;padding:0.3rem 0;">' +
' Unable to reach the server. Retrying&hellip;' +
' </div>' +
' </div>' +
'</div>';
if (typeof lucide !== 'undefined') lucide.createIcons();
if (typeof htmx !== 'undefined') htmx.process(banner);
}
});

// Also catch HTTP errors (5xx) on the banner poll as connection issues
document.addEventListener('htmx:responseError', function(evt) {
if (!evt.detail.elt || !evt.detail.elt.closest('#global-operation-banner')) return;
_diBannerFailCount++;
if (_diBannerFailCount >= 3 && !_diBannerDisconnected) {
// Trigger the same logic — dispatch a synthetic sendError
document.dispatchEvent(new CustomEvent('htmx:sendError', {
detail: { elt: evt.detail.elt }
}));
}
});

// After every HTMX swap: re-render icons, sync animations, one-shot entrance
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target && evt.detail.target.id === 'global-operation-banner') {
// Successful swap — reset connection failure tracking
_diBannerFailCount = 0;
_diBannerDisconnected = false;

if (typeof lucide !== 'undefined') lucide.createIcons();

// Sync looping animations to wall clock so they don't visibly reset on 2s swap.
Expand Down
Loading