fix(web): resolve plugin settings tabs not loading (#301)

* fix(web): resolve plugin settings tabs not loading due to enhancement race

Two co-occurring bugs prevented plugin setting tabs from loading:

1. Both stub-to-full app() enhancement paths (tryEnhance and
   requestAnimationFrame) could fire independently, with the second
   overwriting installedPlugins back to [] after init() already fetched
   them. Added a guard flag (_appEnhanced) and runtime state preservation
   to prevent this race.

2. Plugin config x-init only loaded content if window.htmx was available
   at that exact moment, with no retry or fallback. Added retry loop
   (up to 3s) and fetch() fallback for resilience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): use runtime default tab and add Alpine.initTree to fetch fallback

- Replace hard-coded 'overview' comparison with runtime defaultTab
  (isAPMode ? 'wifi' : 'overview') in both enhancement paths, so
  activeTab is preserved correctly in AP mode
- Add Alpine.initTree(el) call in the plugin config fetch() fallback
  so Alpine directives in the injected HTML are initialized, matching
  the pattern used by loadOverviewDirect and loadWifiDirect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-03-30 12:34:00 -04:00
committed by GitHub
parent 7afc2c0670
commit 68a0fe1182

View File

@@ -519,6 +519,9 @@
}
});
// Guard flag to prevent duplicate stub-to-full enhancement
window._appEnhanced = false;
// Define app() function early so Alpine can find it when it initializes
// This is a complete implementation that will work immediately
(function() {
@@ -534,15 +537,25 @@
init() {
// Try to enhance immediately with full implementation
const tryEnhance = () => {
if (window._appEnhanced) return true;
if (typeof window.app === 'function') {
const fullApp = window.app();
// Check if this is the full implementation (has updatePluginTabs with proper implementation)
if (fullApp && typeof fullApp.updatePluginTabs === 'function' && fullApp.updatePluginTabs.toString().includes('_doUpdatePluginTabs')) {
// Full implementation is available, copy all methods
// But preserve _initialized flag to prevent double init
window._appEnhanced = true;
// Preserve runtime state that should not be reset
const preservedPlugins = this.installedPlugins;
const preservedTab = this.activeTab;
const defaultTab = isAPMode ? 'wifi' : 'overview';
const wasInitialized = this._initialized;
Object.assign(this, fullApp);
// Restore _initialized flag if it was set
// Restore runtime state if non-default
if (preservedPlugins && preservedPlugins.length > 0) {
this.installedPlugins = preservedPlugins;
}
if (preservedTab && preservedTab !== defaultTab) {
this.activeTab = preservedTab;
}
if (wasInitialized) {
this._initialized = wasInitialized;
}
@@ -1253,10 +1266,26 @@
<div class="bg-white rounded-lg shadow p-6 plugin-config-tab"
:id="'plugin-config-' + plugin.id"
x-init="$nextTick(() => {
if (window.htmx && !$el.dataset.htmxLoaded) {
$el.dataset.htmxLoaded = 'true';
htmx.ajax('GET', '/v3/partials/plugin-config/' + plugin.id, {target: $el, swap: 'innerHTML'});
}
const el = $el;
const pid = plugin.id;
const loadContent = (retries) => {
if (window.htmx && !el.dataset.htmxLoaded) {
el.dataset.htmxLoaded = 'true';
htmx.ajax('GET', '/v3/partials/plugin-config/' + pid, {target: el, swap: 'innerHTML'});
} else if (!window.htmx && retries < 15) {
setTimeout(() => loadContent(retries + 1), 200);
} else if (!window.htmx) {
fetch('/v3/partials/plugin-config/' + pid)
.then(r => r.text())
.then(html => {
el.innerHTML = html;
if (window.Alpine) {
window.Alpine.initTree(el);
}
});
}
};
loadContent(0);
})">
<!-- Loading skeleton shown until HTMX loads server-rendered content -->
<div class="animate-pulse space-y-6">
@@ -3066,13 +3095,28 @@
if (window.Alpine) {
// Use requestAnimationFrame for immediate execution without blocking
requestAnimationFrame(() => {
if (window._appEnhanced) return;
window._appEnhanced = true;
const isAPMode = window.location.hostname === '192.168.4.1' ||
window.location.hostname.startsWith('192.168.4.');
const defaultTab = isAPMode ? 'wifi' : 'overview';
const appElement = document.querySelector('[x-data]');
if (appElement && appElement._x_dataStack && appElement._x_dataStack[0]) {
const existingComponent = appElement._x_dataStack[0];
// Preserve runtime state that should not be reset
const preservedPlugins = existingComponent.installedPlugins;
const preservedTab = existingComponent.activeTab;
// Replace all properties and methods from full implementation
Object.keys(fullImplementation).forEach(key => {
existingComponent[key] = fullImplementation[key];
});
// Restore runtime state if non-default
if (preservedPlugins && preservedPlugins.length > 0) {
existingComponent.installedPlugins = preservedPlugins;
}
if (preservedTab && preservedTab !== defaultTab) {
existingComponent.activeTab = preservedTab;
}
// Call init to load plugins and set up watchers (only if not already initialized)
if (typeof existingComponent.init === 'function' && !existingComponent._initialized) {
existingComponent.init();