diff --git a/containers/libreportal/frontend/components/admin/core/css/admin.css b/containers/libreportal/frontend/components/admin/core/css/admin.css index d981645..b34e56c 100644 --- a/containers/libreportal/frontend/components/admin/core/css/admin.css +++ b/containers/libreportal/frontend/components/admin/core/css/admin.css @@ -654,6 +654,7 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); } .sys-detail-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; + gap: 8px; color: rgba(var(--text-rgb), 0.45); font-size: 0.92rem; pointer-events: none; diff --git a/containers/libreportal/frontend/components/admin/overview/js/overview-page.js b/containers/libreportal/frontend/components/admin/overview/js/overview-page.js index 60c5589..c59901b 100644 --- a/containers/libreportal/frontend/components/admin/overview/js/overview-page.js +++ b/containers/libreportal/frontend/components/admin/overview/js/overview-page.js @@ -14,7 +14,7 @@ class OverviewPage { async init() { const r = this.root(); - if (r) r.innerHTML = '
Loading…
'; + if (r) r.innerHTML = '
' + lpLoadingBox() + '
'; this.bindEvents(); const [upd, verify, backup, ssh, disk, mem, info] = await Promise.all([ this.fetchJson('/data/system/update_status.json'), diff --git a/containers/libreportal/frontend/components/admin/ssh/js/ssh-page.js b/containers/libreportal/frontend/components/admin/ssh/js/ssh-page.js index 9a97aa6..561e723 100644 --- a/containers/libreportal/frontend/components/admin/ssh/js/ssh-page.js +++ b/containers/libreportal/frontend/components/admin/ssh/js/ssh-page.js @@ -17,7 +17,7 @@ class SshPage { async init() { const r = this.root(); - if (r) r.innerHTML = '
Loading…
'; + if (r) r.innerHTML = '
' + lpLoadingBox() + '
'; this.bindEvents(); await this.refresh(); this.render(); diff --git a/containers/libreportal/frontend/components/admin/system/js/system-metric-page.js b/containers/libreportal/frontend/components/admin/system/js/system-metric-page.js index 75f2d1b..c3d67db 100644 --- a/containers/libreportal/frontend/components/admin/system/js/system-metric-page.js +++ b/containers/libreportal/frontend/components/admin/system/js/system-metric-page.js @@ -195,7 +195,7 @@ class SystemMetricPage {
-
Loading history…
+
Loading history…
diff --git a/containers/libreportal/frontend/components/admin/system/js/system-page.js b/containers/libreportal/frontend/components/admin/system/js/system-page.js index 6436c25..bcd7dfe 100644 --- a/containers/libreportal/frontend/components/admin/system/js/system-page.js +++ b/containers/libreportal/frontend/components/admin/system/js/system-page.js @@ -84,7 +84,7 @@ class SystemPage { } // Default: index view. - r.innerHTML = '
Loading system stats…
'; + r.innerHTML = '
' + lpLoadingBox('Loading system stats…') + '
'; await this.refresh(); this.bind(); if (window.LiveSystem) { diff --git a/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js b/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js index c057687..c223b02 100755 --- a/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js +++ b/containers/libreportal/frontend/components/apps/core/js/app-tabbed-manager.js @@ -586,7 +586,7 @@ class AppTabbedManager { }); } - section.innerHTML = '
Loading…
'; + section.innerHTML = lpLoadingBox(); await this.appUpdater.refreshAll(); const app = (this.appUpdater.apps || []).find((x) => x.name === this.currentApp); // Always lead with the title block + recessed dark container, so the tab diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js index c09a543..31f9084 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -235,7 +235,7 @@ class OverviewManager { try { window.migratePage.refreshAll().then(() => window.migratePage.render()).catch(() => {}); } catch (_) {} return; } - panel.innerHTML = '
Loading…
'; + panel.innerHTML = lpLoadingBox(); try { if (typeof MigratePage === 'undefined' && window.spaClean && window.spaClean.loadScript) { await window.spaClean.loadScript('/components/apps/overview/migrate/js/migrate-page.js'); @@ -259,7 +259,7 @@ class OverviewManager { try { window.peersPage.refreshAll().then(() => window.peersPage.render()).catch(() => {}); } catch (_) {} return; } - panel.innerHTML = '
Loading…
'; + panel.innerHTML = lpLoadingBox(); try { if (typeof PeersPage === 'undefined' && window.spaClean && window.spaClean.loadScript) { await window.spaClean.loadScript('/components/admin/peers/js/peers-page.js'); @@ -616,7 +616,7 @@ class OverviewManager { } catch (_) {} return; } - pane.innerHTML = '
Loading backup center…
'; + pane.innerHTML = lpLoadingBox('Loading backup center…'); this._loadBackupCenter(pane, sub); } diff --git a/containers/libreportal/frontend/components/backup/configuration/js/backup-engine-details.js b/containers/libreportal/frontend/components/backup/configuration/js/backup-engine-details.js index bc60b75..8caa814 100644 --- a/containers/libreportal/frontend/components/backup/configuration/js/backup-engine-details.js +++ b/containers/libreportal/frontend/components/backup/configuration/js/backup-engine-details.js @@ -42,7 +42,7 @@ Object.assign(BackupPage.prototype, { const row = triggerEl?.closest('.backup-engine-input-row'); const sel = row?.querySelector('select, input'); if (sel && sel.value) engineId = sel.value.trim(); - body.innerHTML = `
Loading engine details…
`; + body.innerHTML = lpLoadingBox('Loading engine details…'); modal.classList.add('open'); const data = await this.fetchJson(`/data/backup/generated/engines/${encodeURIComponent(engineId)}.json?t=${Date.now()}`); diff --git a/containers/libreportal/frontend/core/loading/css/loading-screen.css b/containers/libreportal/frontend/core/loading/css/loading-screen.css index 1cb5ab6..8fcbb1f 100755 --- a/containers/libreportal/frontend/core/loading/css/loading-screen.css +++ b/containers/libreportal/frontend/core/loading/css/loading-screen.css @@ -539,3 +539,52 @@ .error-list-container li { margin-bottom: 8px !important; } + +/* ============================================================ + Shared in-page section loader (window.lpLoadingBox()). + + One consistent "boxed card + spinner + text" used everywhere a + page or panel is fetching data, so every loading state matches + instead of some showing a bare text line with no spinner. Mirrors + the boxed loading card the Services/Config/Tasks tabs already use. + `@keyframes spin` is defined globally in core/theme/css/base.css. + ============================================================ */ +.lp-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.7rem; + padding: 40px 20px; + min-height: 180px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(var(--text-rgb), 0.1); + border-radius: 8px; + color: var(--text-secondary, var(--text-muted)); + font-size: 0.9rem; +} + +.lp-loading-spinner { + width: 22px; + height: 22px; + border: 2px solid rgba(var(--text-rgb), 0.15); + border-top-color: var(--accent-color, var(--accent)); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.lp-loading-text { + line-height: 1.35; + text-align: center; +} + +/* Compact variant for small inline overlays (e.g. metric graphs) — no + box, just a centered spinner + text laid out in a row. */ +.lp-loading.lp-loading-inline { + flex-direction: row; + gap: 8px; + min-height: 0; + padding: 0; + background: none; + border: 0; +} diff --git a/containers/libreportal/frontend/core/loading/js/loading-ui.js b/containers/libreportal/frontend/core/loading/js/loading-ui.js index e4fdaba..168fa2f 100755 --- a/containers/libreportal/frontend/core/loading/js/loading-ui.js +++ b/containers/libreportal/frontend/core/loading/js/loading-ui.js @@ -398,3 +398,17 @@ class LoadingUI { this.systemCards.clear(); } } + +// Shared in-page section loader markup. Returns the HTML for one consistent +// "boxed card + spinner + text" so every page/panel that's fetching data +// shows the same loading state (see core/loading/css/loading-screen.css). +// el.innerHTML = lpLoadingBox(); // default "Loading…" +// el.innerHTML = lpLoadingBox('Loading apps…'); // custom message +// el.innerHTML = lpLoadingBox('Loading…', { inline: true }); // compact, no box +window.lpLoadingBox = function (message = 'Loading…', opts = {}) { + const cls = `lp-loading${opts.inline ? ' lp-loading-inline' : ''}${opts.className ? ` ${opts.className}` : ''}`; + return `
` + + `` + + `
${message}
` + + `
`; +};