mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
fix(web): expose GitHub install handlers, simplify Alpine loader, explicit Flask threading (#305)
A user reported that buttons in the v3 web UI were unresponsive in Safari after a fresh install. The screenshots showed Alpine.js actually running fine end-to-end — the real issues are a narrow handler-exposure bug and some latent brittleness worth cleaning up at the same time. plugins_manager.js: attachInstallButtonHandler and setupGitHubInstallHandlers were declared inside the main IIFE, but the typeof guards that tried to expose them on window ran *outside* the IIFE, so typeof always evaluated to 'undefined' and the assignments were silently skipped. The GitHub "Install from URL" button therefore had no click handler and the console printed [FALLBACK] attachInstallButtonHandler not available on window on every load. Fixed by assigning window.attachInstallButtonHandler and window.setupGitHubInstallHandlers *inside* the IIFE just before it closes, and removing the dead outside-the-IIFE guards. base.html: the Alpine.js loader was a 50-line dynamic-script + deferLoadingAlpine + isAPMode branching block. script.defer = true on a dynamically-inserted <script> is a no-op (dynamic scripts are always async), the deferLoadingAlpine wrapper was cargo-culted, and the AP-mode branching reached out to unpkg unnecessarily on LAN installs even though alpinejs.min.js already ships in web_interface/static/v3/js/. Replaced with a single <script defer src="..."> tag pointing at the local file plus a small window-load rescue that only pulls the CDN copy if window.Alpine is still undefined. start.py / app.py: app.run() has defaulted to threaded=True since Flask 1.0 so this is not a behavior change, but the two long-lived /api/v3/stream/* SSE endpoints would starve every other request under a single-threaded server. Setting threaded=True explicitly makes the intent self-documenting and guards against future regressions. Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -726,4 +726,6 @@ def check_health_monitor():
|
|||||||
_threading.Thread(target=_run_startup_reconciliation, daemon=True).start()
|
_threading.Thread(target=_run_startup_reconciliation, daemon=True).start()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
# threaded=True is Flask's default since 1.0 but stated explicitly so that
|
||||||
|
# long-lived /api/v3/stream/* SSE connections don't starve other requests.
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True, threaded=True)
|
||||||
|
|||||||
@@ -120,7 +120,11 @@ def main():
|
|||||||
|
|
||||||
# Run the web server with error handling for client disconnections
|
# Run the web server with error handling for client disconnections
|
||||||
try:
|
try:
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
# threaded=True is Flask's default since 1.0, but set it explicitly
|
||||||
|
# so it's self-documenting: the two /api/v3/stream/* SSE endpoints
|
||||||
|
# hold long-lived connections and would starve other requests under
|
||||||
|
# a single-threaded server.
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
|
||||||
except (OSError, BrokenPipeError) as e:
|
except (OSError, BrokenPipeError) as e:
|
||||||
# Suppress non-critical socket errors (client disconnections)
|
# Suppress non-critical socket errors (client disconnections)
|
||||||
if isinstance(e, OSError) and e.errno in (113, 32, 104): # No route to host, Broken pipe, Connection reset
|
if isinstance(e, OSError) and e.errno in (113, 32, 104): # No route to host, Broken pipe, Connection reset
|
||||||
|
|||||||
@@ -7161,6 +7161,13 @@ window.getSchemaProperty = getSchemaProperty;
|
|||||||
window.escapeHtml = escapeHtml;
|
window.escapeHtml = escapeHtml;
|
||||||
window.escapeAttribute = escapeAttribute;
|
window.escapeAttribute = escapeAttribute;
|
||||||
|
|
||||||
|
// Expose GitHub install handlers. These must be assigned inside the IIFE —
|
||||||
|
// from outside the IIFE, `typeof attachInstallButtonHandler` evaluates to
|
||||||
|
// 'undefined' and the fallback path at the bottom of this file fires a
|
||||||
|
// [FALLBACK] attachInstallButtonHandler not available on window warning.
|
||||||
|
window.attachInstallButtonHandler = attachInstallButtonHandler;
|
||||||
|
window.setupGitHubInstallHandlers = setupGitHubInstallHandlers;
|
||||||
|
|
||||||
})(); // End IIFE
|
})(); // End IIFE
|
||||||
|
|
||||||
// Functions to handle array-of-objects
|
// Functions to handle array-of-objects
|
||||||
@@ -7390,16 +7397,8 @@ if (typeof loadInstalledPlugins !== 'undefined') {
|
|||||||
if (typeof renderInstalledPlugins !== 'undefined') {
|
if (typeof renderInstalledPlugins !== 'undefined') {
|
||||||
window.renderInstalledPlugins = renderInstalledPlugins;
|
window.renderInstalledPlugins = renderInstalledPlugins;
|
||||||
}
|
}
|
||||||
// Expose GitHub install handlers for debugging and manual testing
|
// GitHub install handlers are now exposed inside the IIFE (see above).
|
||||||
if (typeof setupGitHubInstallHandlers !== 'undefined') {
|
// searchPluginStore is also exposed inside the IIFE after its definition.
|
||||||
window.setupGitHubInstallHandlers = setupGitHubInstallHandlers;
|
|
||||||
console.log('[GLOBAL] setupGitHubInstallHandlers exposed to window');
|
|
||||||
}
|
|
||||||
if (typeof attachInstallButtonHandler !== 'undefined') {
|
|
||||||
window.attachInstallButtonHandler = attachInstallButtonHandler;
|
|
||||||
console.log('[GLOBAL] attachInstallButtonHandler exposed to window');
|
|
||||||
}
|
|
||||||
// searchPluginStore is now exposed inside the IIFE after its definition
|
|
||||||
|
|
||||||
// Verify critical functions are available
|
// Verify critical functions are available
|
||||||
if (_PLUGIN_DEBUG_EARLY) {
|
if (_PLUGIN_DEBUG_EARLY) {
|
||||||
|
|||||||
@@ -786,56 +786,25 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Alpine.js for reactive components -->
|
<!-- Alpine.js for reactive components.
|
||||||
<!-- Use local file when in AP mode (192.168.4.x) to avoid CDN dependency -->
|
Load the local copy first (always works, no CDN round-trip, no AP-mode
|
||||||
|
branch needed). `defer` on an HTML-parsed <script> is honored and runs
|
||||||
|
after DOM parse but before DOMContentLoaded, which is exactly what
|
||||||
|
Alpine wants — so no deferLoadingAlpine gymnastics are needed.
|
||||||
|
The inline rescue below only fires if the local file is missing. -->
|
||||||
|
<script defer src="{{ url_for('static', filename='v3/js/alpinejs.min.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
// Rescue: if the local Alpine didn't load for any reason, pull the CDN
|
||||||
// Prevent Alpine from auto-initializing by setting deferLoadingAlpine before it loads
|
// copy once on window load. This is a last-ditch fallback, not the
|
||||||
window.deferLoadingAlpine = function(callback) {
|
// primary path.
|
||||||
// Wait for DOM to be ready
|
window.addEventListener('load', function() {
|
||||||
function waitForReady() {
|
if (typeof window.Alpine === 'undefined') {
|
||||||
if (document.readyState === 'loading') {
|
console.warn('[Alpine] Local file failed to load, falling back to CDN');
|
||||||
document.addEventListener('DOMContentLoaded', waitForReady);
|
const s = document.createElement('script');
|
||||||
return;
|
s.src = 'https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js';
|
||||||
}
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
// app() is already defined in head, so we can initialize Alpine
|
});
|
||||||
if (callback && typeof callback === 'function') {
|
|
||||||
callback();
|
|
||||||
} else if (window.Alpine && typeof window.Alpine.start === 'function') {
|
|
||||||
// If callback not provided but Alpine is available, start it
|
|
||||||
try {
|
|
||||||
window.Alpine.start();
|
|
||||||
} catch (e) {
|
|
||||||
// Alpine may already be initialized, ignore
|
|
||||||
console.warn('Alpine start error (may already be initialized):', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForReady();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Detect AP mode by IP address
|
|
||||||
const isAPMode = window.location.hostname === '192.168.4.1' ||
|
|
||||||
window.location.hostname.startsWith('192.168.4.');
|
|
||||||
|
|
||||||
const alpineSrc = isAPMode ? '/static/v3/js/alpinejs.min.js' : 'https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js';
|
|
||||||
const alpineFallback = isAPMode ? 'https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js' : '/static/v3/js/alpinejs.min.js';
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.defer = true;
|
|
||||||
script.src = alpineSrc;
|
|
||||||
script.onerror = function() {
|
|
||||||
if (alpineSrc !== alpineFallback) {
|
|
||||||
const fallback = document.createElement('script');
|
|
||||||
fallback.defer = true;
|
|
||||||
fallback.src = alpineFallback;
|
|
||||||
document.head.appendChild(fallback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.head.appendChild(script);
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- CodeMirror for JSON editing - lazy loaded when needed -->
|
<!-- CodeMirror for JSON editing - lazy loaded when needed -->
|
||||||
|
|||||||
Reference in New Issue
Block a user