From b3b9f9a18b461febc6731ae0414a3fa6041fef4a Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 21 Aug 2026 00:28:36 +0100 Subject: [PATCH] fix(webui): stop the loader double-loading scripts, and the sidebar noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two recurring console errors, each masking real ones. "Identifier 'BackupAppCard' has already been declared": the app has two script loaders that could not see each other's work. The boot-time system loader injects tags with NO id and dedupes by script[src]; the SPA's loadScript injects with an id and deduped ONLY by that id. So a file both of them load — backup-app-card.js is on the boot list AND in the overview Backups tab's asset chain — loaded twice whenever the SPA loaded second: its getElementById check cannot see an id-less tag. The class redeclared with an uncaught SyntaxError. The blindness was one-directional, which is why it looked sporadic. spaClean.loadScript now also matches by src, closing the asymmetry for every shared file, not just this one. "ConfigSidebar: config-categories-list element not found", three to four times per navigation: renderConfig populated the admin sidebar unconditionally, but the backup center's Configuration tab EMBEDS that renderer inside its own pane (renderConfig('backup', )), where the sidebar element legitimately does not exist — and the refresh coordinator re-renders that tab on every task event. Populating is now skipped when a target is passed: an embedded render does not own the admin shell. The error stays for target-less renders, where a missing sidebar genuinely is a broken shell. Verified in a live session: repeated sweeps across /tasks, the overview Backups tab (Configuration sub-tab included) and a per-app Backups page produce zero new occurrences of either error — confirmed against a live console buffer via a sentinel, since the buffer retains the pre-fix history and re-reading it can look like recurrence. Co-Authored-By: Claude Opus 5 --- .../components/admin/config/js/config-manager.js | 9 +++++++-- containers/libreportal/frontend/core/kernel/js/spa.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) 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 22cd309..24c87b3 100755 --- a/containers/libreportal/frontend/components/admin/config/js/config-manager.js +++ b/containers/libreportal/frontend/components/admin/config/js/config-manager.js @@ -162,8 +162,13 @@ if (typeof window.ConfigManager === 'undefined') { // Load configuration data const configData = await this.core.loadConfig(category); - // Populate sidebar with categories - this.sidebar.populateSidebar(); + // Populate sidebar with categories — but only when rendering into the + // admin shell. A `target` means an EMBEDDED render (the backup center's + // Configuration tab reuses this renderer inside its own pane), where + // config-categories-list legitimately does not exist; populating there + // logged "element not found" as an error on every repaint, and the + // refresh coordinator repaints on every task event. + if (!target) this.sidebar.populateSidebar(); if (Object.keys(configData).length === 0) { configSection.innerHTML = '

No Configuration Available

No configuration items found for this category.

'; diff --git a/containers/libreportal/frontend/core/kernel/js/spa.js b/containers/libreportal/frontend/core/kernel/js/spa.js index a5deaab..a54e3a1 100755 --- a/containers/libreportal/frontend/core/kernel/js/spa.js +++ b/containers/libreportal/frontend/core/kernel/js/spa.js @@ -528,6 +528,16 @@ class LibrePortalSPAClean { if (document.getElementById(scriptId)) { return; // Already loaded } + // Also recognise tags the boot-time system loader injected: it dedupes by + // src and adds NO id, so the id check above cannot see its scripts. That + // one-way blindness is how backup-app-card.js loaded twice — boot loads it + // id-less, the overview's Backups tab loads it again through here, and + // `class BackupAppCard` redeclares with an uncaught SyntaxError. The system + // loader's own attribute check already covers scripts WE inject, so + // matching by src here closes the asymmetry for every shared file. + if (document.querySelector(`script[src="${src}"]`)) { + return; + } return new Promise((resolve, reject) => { const script = document.createElement('script');