Preserve table content on API errors with header warning badge

Instead of replacing the entire table with an error alert on network
failures, the previous data now stays visible and a warning icon with
tooltip appears in the sub-header bar. The warning clears automatically
on the next successful poll.

Restructured page headers into title row + sub-header row (count,
auto-refresh, warning) for better mobile layout.
This commit is contained in:
Louis King
2026-04-26 14:03:26 +01:00
parent e4655a6b59
commit 8dec688917
4 changed files with 78 additions and 30 deletions
@@ -8,6 +8,7 @@ import { html, nothing } from 'lit-html';
import { render } from 'lit-html';
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { t } from './i18n.js';
import { iconAlert } from './icons.js';
// Re-export lit-html utilities for page modules
export { html, nothing, unsafeHTML };
@@ -381,6 +382,17 @@ export function successAlert(message) {
</div>`;
}
/**
* Render a warning badge with tooltip for transient API errors.
* @param {string} message - Error message to display as tooltip
* @returns {TemplateResult}
*/
export function warningBadge(message) {
return html`<span class="tooltip tooltip-bottom" data-tip="${message}">
<span class="badge badge-warning badge-sm">${iconAlert('h-4 w-4')}</span>
</span>`;
}
/**
* Render pagination controls.
* @param {number} page - Current page (1-based)
@@ -2,7 +2,7 @@ import { apiGet } from '../api.js';
import {
html, litRender, nothing, t,
getConfig, formatDateTime, formatDateTimeShort, formatRelativeTime,
truncateKey, errorAlert,
truncateKey, warningBadge,
pagination, createFilterHandler, autoSubmit, submitOnEnter, copyToClipboard, renderNodeDisplay,
observerIcons, observerDetailRow, toggleObserverDetail, toggleCardObserverDetail
} from '../components.js';
@@ -24,17 +24,29 @@ export async function render(container, params, router) {
const tzBadge = tz && tz !== 'UTC' ? html`<span class="text-sm opacity-60">${tz}</span>` : nothing;
const navigate = (url) => router.navigate(url);
function renderPage(content, { total = null } = {}) {
let lastContent = nothing;
let lastTotal = null;
function renderPage(content, { total = null, error = null } = {}) {
if (!error) {
lastContent = content;
lastTotal = total;
}
const displayContent = error ? lastContent : content;
const displayTotal = error ? lastTotal : total;
litRender(html`
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold">${t('entities.advertisements')}</h1>
<div class="flex items-center gap-2">
<span id="auto-refresh-toggle"></span>
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
</div>
${tzBadge}
</div>
${content}`, container);
<div class="flex items-center gap-2 mb-4">
${displayTotal !== null
? html`<span class="badge badge-lg">${t('common.total', { count: displayTotal })}</span>`
: nothing}
<span id="auto-refresh-toggle"></span>
${error ? warningBadge(error) : nothing}
</div>
${displayContent}`, container);
}
// Render page header immediately (old content stays visible until data loads)
@@ -223,7 +235,7 @@ ${content}`, container);
${paginationBlock}`, { total });
} catch (e) {
renderPage(errorAlert(e.message));
renderPage(nothing, { error: e.message });
}
}
@@ -3,7 +3,7 @@ import {
html, litRender, nothing, t,
getConfig, formatDateTime, formatDateTimeShort, formatRelativeTime,
getChannelLabelsMap, resolveChannelLabel,
truncateKey, errorAlert,
truncateKey, warningBadge,
pagination, timezoneIndicator,
createFilterHandler, autoSubmit, submitOnEnter,
observerIcons, observerDetailRow, toggleObserverDetail, toggleCardObserverDetail
@@ -169,17 +169,29 @@ export async function render(container, params, router) {
return deduped;
}
function renderPage(content, { total = null } = {}) {
let lastContent = nothing;
let lastTotal = null;
function renderPage(content, { total = null, error = null } = {}) {
if (!error) {
lastContent = content;
lastTotal = total;
}
const displayContent = error ? lastContent : content;
const displayTotal = error ? lastTotal : total;
litRender(html`
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold">${t('entities.messages')}</h1>
<div class="flex items-center gap-2">
<span id="auto-refresh-toggle"></span>
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
</div>
${tzBadge}
</div>
${content}`, container);
<div class="flex items-center gap-2 mb-4">
${displayTotal !== null
? html`<span class="badge badge-lg">${t('common.total', { count: displayTotal })}</span>`
: nothing}
<span id="auto-refresh-toggle"></span>
${error ? warningBadge(error) : nothing}
</div>
${displayContent}`, container);
}
// Render page header immediately (old content stays visible until data loads)
@@ -348,7 +360,7 @@ ${content}`, container);
${paginationBlock}`, { total });
} catch (e) {
renderPage(errorAlert(e.message));
renderPage(nothing, { error: e.message });
}
}
@@ -2,7 +2,7 @@ import { apiGet } from '../api.js';
import {
html, litRender, nothing,
getConfig, formatDateTime, formatDateTimeShort,
truncateKey, errorAlert,
truncateKey, warningBadge,
pagination, timezoneIndicator,
createFilterHandler, autoSubmit, submitOnEnter, copyToClipboard, renderNodeDisplay, t
} from '../components.js';
@@ -24,17 +24,29 @@ export async function render(container, params, router) {
const tzBadge = tz && tz !== 'UTC' ? html`<span class="text-sm opacity-60">${tz}</span>` : nothing;
const navigate = (url) => router.navigate(url);
function renderPage(content, { total = null } = {}) {
let lastContent = nothing;
let lastTotal = null;
function renderPage(content, { total = null, error = null } = {}) {
if (!error) {
lastContent = content;
lastTotal = total;
}
const displayContent = error ? lastContent : content;
const displayTotal = error ? lastTotal : total;
litRender(html`
<div class="flex items-center justify-between mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold">${t('entities.nodes')}</h1>
<div class="flex items-center gap-2">
<span id="auto-refresh-toggle"></span>
${tzBadge}
${total !== null ? html`<span class="badge badge-lg">${t('common.total', { count: total })}</span>` : nothing}
</div>
${tzBadge}
</div>
${content}`, container);
<div class="flex items-center gap-2 mb-4">
${displayTotal !== null
? html`<span class="badge badge-lg">${t('common.total', { count: displayTotal })}</span>`
: nothing}
<span id="auto-refresh-toggle"></span>
${error ? warningBadge(error) : nothing}
</div>
${displayContent}`, container);
}
// Render page header immediately (old content stays visible until data loads)
@@ -195,7 +207,7 @@ ${content}`, container);
${paginationBlock}`, { total });
} catch (e) {
renderPage(errorAlert(e.message));
renderPage(nothing, { error: e.message });
}
}