/* Dashboard widget cells: say when a cell is loading, when it could not load, and how fresh what it shows is (#5208). Each .dash-cell-body is an htmx target with a request deadline (hx-request timeout), so every fetch ends in a swap or one of the error events below. A cell that polls keeps polling through htmx's own `every` trigger; a cell that loads once is retried here, re-armed from the failure handler of each attempt, so a failed retry arms the next one. */ (function () { var RETRY_SECONDS = 30; function hhmm(d) { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function asofOf(cell) { var box = cell.closest('.grid-stack-item-content, .dash-cell'); return box ? box.querySelector('.widget-asof') : null; } function isCell(el) { return el && el.classList && el.classList.contains('dash-cell-body'); } function scheduleRetry(cell) { clearTimeout(cell._retry); if (cell.dataset.poll === '1') return; // its own poll is the retry cell._retry = setTimeout(function () { htmx.trigger(cell, 'retry'); }, RETRY_SECONDS * 1000); } function failed(cell, why) { var asof = asofOf(cell); var when = cell.dataset.loadedAt ? new Date(+cell.dataset.loadedAt) : null; if (when) { // Keep what it showed; say it is no longer current. if (asof) { asof.textContent = "couldn't refresh ยท as of " + hhmm(when); asof.classList.add('widget-asof-failed'); asof.title = why; } } else { var retryLine = cell.dataset.poll === '1' ? 'Trying again with the next refresh.' : 'Trying again in ' + RETRY_SECONDS + 's.'; cell.innerHTML = ''; var box = document.createElement('div'); box.className = 'cell-state cell-state-failed'; var line = document.createElement('div'); line.textContent = "Couldn't load this widget (" + why + '). ' + retryLine; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm'; btn.textContent = 'Retry now'; btn.addEventListener('click', function () { clearTimeout(cell._retry); htmx.trigger(cell, 'retry'); }); box.appendChild(line); box.appendChild(btn); cell.appendChild(box); } scheduleRetry(cell); } document.body.addEventListener('htmx:afterSwap', function (e) { var cell = e.detail.target; if (!isCell(cell)) return; clearTimeout(cell._retry); var now = new Date(); cell.dataset.loadedAt = String(now.getTime()); var asof = asofOf(cell); if (asof) { asof.textContent = 'as of ' + hhmm(now); asof.classList.remove('widget-asof-failed'); asof.title = 'Loaded ' + now.toLocaleString(); } }); document.body.addEventListener('htmx:responseError', function (e) { if (isCell(e.detail.target)) failed(e.detail.target, 'HTTP ' + e.detail.xhr.status); }); document.body.addEventListener('htmx:sendError', function (e) { if (isCell(e.detail.target)) failed(e.detail.target, 'no connection'); }); document.body.addEventListener('htmx:timeout', function (e) { if (isCell(e.detail.target)) failed(e.detail.target, 'timed out'); }); })();