From 8dec688917dd53e3f3e475254a07e6b9d51c6bdd Mon Sep 17 00:00:00 2001 From: Louis King Date: Sun, 26 Apr 2026 14:03:26 +0100 Subject: [PATCH] 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. --- .../web/static/js/spa/components.js | 12 +++++++ .../web/static/js/spa/pages/advertisements.js | 32 +++++++++++++------ .../web/static/js/spa/pages/messages.js | 32 +++++++++++++------ .../web/static/js/spa/pages/nodes.js | 32 +++++++++++++------ 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/meshcore_hub/web/static/js/spa/components.js b/src/meshcore_hub/web/static/js/spa/components.js index aa18d3c..eb3af35 100644 --- a/src/meshcore_hub/web/static/js/spa/components.js +++ b/src/meshcore_hub/web/static/js/spa/components.js @@ -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) { `; } +/** + * 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` + ${iconAlert('h-4 w-4')} + `; +} + /** * Render pagination controls. * @param {number} page - Current page (1-based) diff --git a/src/meshcore_hub/web/static/js/spa/pages/advertisements.js b/src/meshcore_hub/web/static/js/spa/pages/advertisements.js index 0c18aee..c62e800 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/advertisements.js +++ b/src/meshcore_hub/web/static/js/spa/pages/advertisements.js @@ -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`${tz}` : 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` -
+

${t('entities.advertisements')}

-
- - ${tzBadge} - ${total !== null ? html`${t('common.total', { count: total })}` : nothing} -
+ ${tzBadge}
-${content}`, container); +
+ ${displayTotal !== null + ? html`${t('common.total', { count: displayTotal })}` + : nothing} + + ${error ? warningBadge(error) : nothing} +
+${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 }); } } diff --git a/src/meshcore_hub/web/static/js/spa/pages/messages.js b/src/meshcore_hub/web/static/js/spa/pages/messages.js index fed6bc6..9b0a41b 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/messages.js +++ b/src/meshcore_hub/web/static/js/spa/pages/messages.js @@ -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` -
+

${t('entities.messages')}

-
- - ${tzBadge} - ${total !== null ? html`${t('common.total', { count: total })}` : nothing} -
+ ${tzBadge}
-${content}`, container); +
+ ${displayTotal !== null + ? html`${t('common.total', { count: displayTotal })}` + : nothing} + + ${error ? warningBadge(error) : nothing} +
+${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 }); } } diff --git a/src/meshcore_hub/web/static/js/spa/pages/nodes.js b/src/meshcore_hub/web/static/js/spa/pages/nodes.js index e6a072c..0c0f529 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/nodes.js +++ b/src/meshcore_hub/web/static/js/spa/pages/nodes.js @@ -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`${tz}` : 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` -
+

${t('entities.nodes')}

-
- - ${tzBadge} - ${total !== null ? html`${t('common.total', { count: total })}` : nothing} -
+ ${tzBadge}
-${content}`, container); +
+ ${displayTotal !== null + ? html`${t('common.total', { count: displayTotal })}` + : nothing} + + ${error ? warningBadge(error) : nothing} +
+${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 }); } }