fix(apps): stop re-serialising the config section, which killed its listeners

Every themed dropdown on the app config page was dead: it rendered, but
clicking did nothing. Only that page — every other dropdown in the WebUI
worked.

renderAppDetail captured the config section's own innerHTML right after
displayConfigForm() had rendered it...

  const configHTML = document.getElementById('config-section')?.innerHTML;
  ...67 lines later...
  configSection.innerHTML = configHTML;

...and wrote the same string straight back. That is a no-op for the
markup and a catastrophe for behaviour: re-assigning innerHTML re-parses
the subtree, so every listener in it is destroyed.

custom-select.js had already wrapped each <select>, so the captured
string contained the .custom-select wrapper and the custom-select-native
class. The re-inserted copy therefore looked enhanced — which made the
enhancer correctly skip it as already-done — while having no click
handler at all. A dropdown that renders perfectly and does nothing.

The container is never wholesale-rewritten in that function (it updates
header/config/console individually), so the capture-and-restore had no
purpose. Both lines removed, with a comment stating that anything added
there must mutate the section rather than re-assign its innerHTML.

Diagnosed in a real browser rather than by reading: instrumenting
CustomSelect.build() showed the enhancer DID build a widget for the field
while the wrapper in the DOM was not the one it built, and patching the
innerHTML setter named apps-manager.js:785 as the writer. Verified live:
the dropdown opens, and picking an option sets the value (auto) and the
label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-12 23:09:25 +01:00
parent 07daa7555a
commit 47b614f8b4

View File

@ -714,8 +714,6 @@ class AppsManager {
// Render config section with working app-config-original.js approach
// Use the working displayConfigForm from app-config-original.js
await this.displayConfigForm(app, preferredCategory);
const configHTML = document.getElementById('config-section')?.innerHTML || '';
// Initialize port managers after config form is rendered
setTimeout(async () => {
@ -778,15 +776,17 @@ class AppsManager {
}
}
// Update config section
const configSection = document.getElementById('config-section');
//// // console.log('config-section element found:', !!configSection);
if (configSection) {
configSection.innerHTML = configHTML;
//// // console.log('Config section updated successfully, innerHTML length:', configHTML.length);
} else {
console.error('config-section element not found in DOM');
}
// The config section is DELIBERATELY not touched here. displayConfigForm()
// above already rendered it, and this used to capture its innerHTML and
// write the same string back a few lines later. That round-trip is a no-op
// for the markup but re-parses the subtree, so every listener attached to
// it is destroyed — which silently killed every themed dropdown on the app
// config page: custom-select.js had already wrapped each <select>, so the
// re-inserted copy carried the .custom-select markup (and therefore was
// skipped by the enhancer as "already done") while having no click handler.
// The container is never wholesale-rewritten in this function, so there is
// nothing to restore. Anything added here must MUTATE the section, never
// re-assign its innerHTML.
// Update console section (preserve original structure)
const consoleSection = container.querySelector('.console-section');