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; }