mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
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>
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LED Matrix Web Interface V3 Startup Script
|
|
Modern web interface with real-time display preview and plugin management.
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
def get_local_ips():
|
|
"""Get list of local IP addresses the service will be accessible on."""
|
|
ips = []
|
|
|
|
# Check if AP mode is active
|
|
try:
|
|
result = subprocess.run(
|
|
["systemctl", "is-active", "hostapd"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip() == "active":
|
|
ips.append("192.168.4.1 (AP Mode)")
|
|
except Exception:
|
|
pass
|
|
|
|
# Get IPs from hostname -I
|
|
try:
|
|
result = subprocess.run(
|
|
["hostname", "-I"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2
|
|
)
|
|
if result.returncode == 0:
|
|
for ip in result.stdout.strip().split():
|
|
ip = ip.strip()
|
|
if ip and not ip.startswith("127.") and ip != "192.168.4.1":
|
|
ips.append(ip)
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: try socket method
|
|
if not ips:
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
try:
|
|
s.connect(('8.8.8.8', 80))
|
|
ip = s.getsockname()[0]
|
|
if ip and not ip.startswith("127."):
|
|
ips.append(ip)
|
|
finally:
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
|
|
return ips if ips else ["localhost"]
|
|
|
|
def main():
|
|
"""Main startup function."""
|
|
# Change to project root directory
|
|
project_root = Path(__file__).parent.parent
|
|
os.chdir(project_root)
|
|
|
|
# Add to Python path
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Configure logging to suppress non-critical socket errors
|
|
# These occur when clients disconnect and are harmless
|
|
werkzeug_logger = logging.getLogger('werkzeug')
|
|
original_log_exception = werkzeug_logger.error
|
|
|
|
def log_exception_filtered(message, *args, **kwargs):
|
|
"""Filter out non-critical socket errors from werkzeug logs."""
|
|
if isinstance(message, str):
|
|
# Suppress "No route to host" and similar connection errors
|
|
if 'No route to host' in message or 'errno 113' in message:
|
|
# Log at debug level instead of error
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# Suppress broken pipe errors (client disconnected)
|
|
if 'Broken pipe' in message or 'errno 32' in message:
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# For exceptions, check if it's a socket error
|
|
if 'exc_info' in kwargs and kwargs['exc_info']:
|
|
exc_type, exc_value, exc_tb = kwargs['exc_info']
|
|
if isinstance(exc_value, OSError):
|
|
# Suppress common non-critical socket errors
|
|
if exc_value.errno in (113, 32, 104): # No route to host, Broken pipe, Connection reset
|
|
werkzeug_logger.debug(message, *args, **kwargs)
|
|
return
|
|
# Log everything else normally
|
|
original_log_exception(message, *args, **kwargs)
|
|
|
|
werkzeug_logger.error = log_exception_filtered
|
|
|
|
# Import and run the Flask app
|
|
from web_interface.app import app
|
|
|
|
print("Starting LED Matrix Web Interface V3...")
|
|
print("Web server binding to: 0.0.0.0:5000")
|
|
|
|
# Get and display accessible IP addresses
|
|
ips = get_local_ips()
|
|
if ips:
|
|
print("Access the interface at:")
|
|
for ip in ips:
|
|
if "AP Mode" in ip:
|
|
print(f" - http://192.168.4.1:5000 (AP Mode - connect to LEDMatrix-Setup WiFi)")
|
|
else:
|
|
print(f" - http://{ip}:5000")
|
|
else:
|
|
print(" - http://localhost:5000 (local only)")
|
|
print(" - http://<your-pi-ip>:5000 (replace with your Pi's IP address)")
|
|
|
|
# Run the web server with error handling for client disconnections
|
|
try:
|
|
# 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:
|
|
# 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
|
|
werkzeug_logger.debug(f"Client disconnected: {e}", exc_info=True)
|
|
# Re-raise only if it's not a client disconnection error
|
|
if e.errno not in (113, 32, 104):
|
|
raise
|
|
else:
|
|
raise
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|