diff --git a/containers/libreportal/frontend/components/admin/config/js/config-manager.js b/containers/libreportal/frontend/components/admin/config/js/config-manager.js
index 38c29ba..22cd309 100755
--- a/containers/libreportal/frontend/components/admin/config/js/config-manager.js
+++ b/containers/libreportal/frontend/components/admin/config/js/config-manager.js
@@ -16,9 +16,17 @@ if (typeof window.ConfigManager === 'undefined') {
window.IPWhitelistManager = this.whitelistManager;
}
- async renderConfig(category) {
-
- const configSection = document.getElementById('config-section');
+ async renderConfig(category, target) {
+
+ // `target` (an element or an element id) lets an embedder render into its
+ // OWN container instead of the page-global #config-section. The shared
+ // apps-unified-layout keeps a hidden #config-tab > #config-section in the
+ // DOM at all times, so on the Fleet Overview a plain
+ // getElementById('config-section') resolves to THAT stale element (two
+ // nodes share the id) and the form paints where nobody can see it. The
+ // Backups tab's Configuration sub-tab passes its own container id here.
+ const configSection = (typeof target === 'string' ? document.getElementById(target) : target)
+ || document.getElementById('config-section');
if (!configSection) {
console.error('ConfigManager: config-section element not found');
return;
@@ -238,7 +246,7 @@ if (typeof window.ConfigManager === 'undefined') {
} catch (error) {
console.error('ConfigManager: Error rendering ' + category + ' config: ', error);
- configSection.innerHTML = '
Error Loading Configuration
Failed to load configuration: ' + error.message + '
';
+ configSection.innerHTML = '
Error Loading Configuration
Failed to load configuration: ' + error.message + '
';
}
}
diff --git a/containers/libreportal/frontend/components/backup/configuration/js/backup-configuration.js b/containers/libreportal/frontend/components/backup/configuration/js/backup-configuration.js
index 0d03f3d..72b7c3e 100644
--- a/containers/libreportal/frontend/components/backup/configuration/js/backup-configuration.js
+++ b/containers/libreportal/frontend/components/backup/configuration/js/backup-configuration.js
@@ -29,9 +29,14 @@ Object.assign(BackupPage.prototype, {
`;
+ // Unique id (NOT the page-global "config-section"): the shared
+ // apps-unified-layout keeps a hidden #config-tab > #config-section in the
+ // DOM, so reusing that id would collide and the config form would render
+ // into the wrong (invisible) node. renderConfig() is told to target this
+ // container explicitly.
body.innerHTML = `
${warningHTML}
-
+
`;
this.invokeConfigManager();
@@ -39,7 +44,7 @@ Object.assign(BackupPage.prototype, {
async invokeConfigManager(attempt = 0) {
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
try {
- await window.configManager.renderConfig('backup');
+ await window.configManager.renderConfig('backup', 'backup-config-section');
this.enhanceConfigurationWithPresets();
} catch (err) {
console.error('Backup configuration render failed:', err);
@@ -47,7 +52,7 @@ Object.assign(BackupPage.prototype, {
return;
}
if (attempt >= 20) {
- const sec = document.getElementById('config-section');
+ const sec = document.getElementById('backup-config-section');
if (sec) sec.innerHTML = `
Configuration system not loaded. Try refreshing the page.
`;
return;
}
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 8caa814..633a59f 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
@@ -2,7 +2,7 @@
Object.assign(BackupPage.prototype, {
enhanceEngineDetailsButton() {
const selector = '[name="CFG_BACKUP_ENGINE"], [name^="CFG_BACKUP_LOC_"][name$="_ENGINE"]';
- document.querySelectorAll(`#config-section ${selector}, .backup-location-details ${selector}`).forEach((engineInput) => {
+ document.querySelectorAll(`#backup-config-section ${selector}, .backup-location-details ${selector}`).forEach((engineInput) => {
const customSelect = engineInput.closest('.custom-select');
const wrapTarget = customSelect || engineInput;
const group = wrapTarget.closest('.field-group') || wrapTarget.parentElement;
diff --git a/containers/libreportal/frontend/components/backup/configuration/js/backup-retention-presets.js b/containers/libreportal/frontend/components/backup/configuration/js/backup-retention-presets.js
index f23492d..46b33eb 100644
--- a/containers/libreportal/frontend/components/backup/configuration/js/backup-retention-presets.js
+++ b/containers/libreportal/frontend/components/backup/configuration/js/backup-retention-presets.js
@@ -2,7 +2,7 @@
Object.assign(BackupPage.prototype, {
enhanceConfigurationWithPresets() {
this.enhanceEngineDetailsButton();
- const lastInput = document.querySelector('#config-section [name="CFG_BACKUP_KEEP_LAST"]');
+ const lastInput = document.querySelector('#backup-config-section [name="CFG_BACKUP_KEEP_LAST"]');
if (!lastInput) return;
const section = lastInput.closest('.config-category');
diff --git a/containers/libreportal/frontend/components/backup/core/css/backup.css b/containers/libreportal/frontend/components/backup/core/css/backup.css
index ca99624..84b995e 100755
--- a/containers/libreportal/frontend/components/backup/core/css/backup.css
+++ b/containers/libreportal/frontend/components/backup/core/css/backup.css
@@ -86,6 +86,19 @@
background: rgba(var(--text-rgb), 0.1);
}
+/* Busy state while a manual refresh is in flight — spin the arrow icon and
+ lock the button so the click gives visible feedback (refreshAll() + re-render
+ is otherwise silent, which read as "the button does nothing"). `spin` is the
+ global keyframe from core/theme/css/base.css. */
+.backup-refresh-btn.is-refreshing {
+ pointer-events: none;
+ opacity: 0.75;
+}
+.backup-refresh-btn.is-refreshing svg {
+ animation: spin 0.8s linear infinite;
+ transform-origin: center;
+}
+
.backup-tabpanel {
display: none;
/* Content inset matching the canonical .tab-panel (5px 24px), so panels
diff --git a/containers/libreportal/frontend/components/backup/core/js/backup-page.js b/containers/libreportal/frontend/components/backup/core/js/backup-page.js
index e2098c7..73153cf 100644
--- a/containers/libreportal/frontend/components/backup/core/js/backup-page.js
+++ b/containers/libreportal/frontend/components/backup/core/js/backup-page.js
@@ -118,8 +118,16 @@ class BackupPage {
return;
}
- if (e.target.closest('#backup-refresh-btn')) {
- this.refreshAll().then(() => this.render());
+ const refreshBtn = e.target.closest('#backup-refresh-btn');
+ if (refreshBtn) {
+ // Give the click visible feedback: spin + lock the button until
+ // the refetch + re-render settle. Without this the button looks
+ // dead because refreshAll()/render() paint nothing new on a tab
+ // whose data hasn't changed.
+ if (refreshBtn.classList.contains('is-refreshing')) return;
+ refreshBtn.classList.add('is-refreshing');
+ const done = () => refreshBtn.classList.remove('is-refreshing');
+ this.refreshAll().then(() => this.render()).then(done, done);
return;
}