mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(web): plugin dir resolver in routes, nmcli AP detection, daemon config reload, upload safety, BDF preview (#655)
* fix(web): plugin dir resolver in routes, nmcli AP detection, daemon config reload, upload safety - Route plugin lookups (installed list, update, recorded version, config form, web UI pages) through the plugin manager's resolver so plugins in ledmatrix-<id> directories work. - Captive-portal detection also sees the nmcli fallback AP (cached). - WiFi monitor daemon re-reads wifi_config.json when its mtime changes. - Drop the AP check in disconnect_from_network that could never fire. - LED status file per WiFiManager; config path falls back to this checkout. - BDF font preview via src.common.bdf_font. - Asset uploads validate every file before saving; metadata and calendar credentials written atomically; no absolute path in the response; asset delete answers 400 for a missing body. - Coerce string booleans in plugin toggle, on-demand start and AP force. - SSE broadcaster clears its thread handle before exiting. - start.py log filter handles every exc_info form. - Cleanups: unused plugins/fonts partial work, duplicate backup catch-alls, raw-config error helper, update-route tidy, redundant imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): request BDF font previews now that the server renders them The Fonts tab skipped the preview request for .bdf files because the server used to refuse them; /fonts/preview now draws BDF with the shared loader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): take the update route's plugin directory from a directory listing CodeQL flagged the path built from the request's plugin_id (the id was already validated with safe_path_component, which CodeQL doesn't model; the same flow on main is alerts 738/739). The directory is now the entry of plugins_dir matched by name, so nothing built from user input reaches the filesystem; an id with nothing installed goes to the store manager, which reports it not found as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): read the blueprint's plugin_manager defensively in _plugin_directory _get_plugin_version now goes through _plugin_directory, which read api_v3.plugin_manager directly; the attribute exists only once the app sets it, so test_path_traversal_guards::test_a_real_manifest_is_read failed when run on its own (order-dependent in the full suite). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+36
-37
@@ -44,14 +44,12 @@ def get_wifi_config_path():
|
||||
# Try to determine project root
|
||||
project_root = os.environ.get('LEDMATRIX_ROOT')
|
||||
if not project_root:
|
||||
# Try to find project root by looking for config directory
|
||||
current = Path(__file__).resolve().parent.parent
|
||||
if (current / 'config').exists():
|
||||
project_root = str(current)
|
||||
else:
|
||||
# Fallback to common location
|
||||
project_root = "/home/ledpi/LEDMatrix"
|
||||
|
||||
# This file is <root>/src/wifi_manager.py. The root is used even when
|
||||
# config/ does not exist yet (WiFiManager creates it): the old
|
||||
# fallback was a hardcoded /home/ledpi/LEDMatrix, which is some other
|
||||
# user's checkout, or nothing, on any install not made as ledpi.
|
||||
project_root = str(Path(__file__).resolve().parents[1])
|
||||
|
||||
return Path(project_root) / "config" / "wifi_config.json"
|
||||
|
||||
|
||||
@@ -90,8 +88,11 @@ AP_PROFILE_NAME = "LEDMatrix-Setup-AP"
|
||||
#: Deleted by name only, never by SSID, so a saved home network is never hit.
|
||||
AP_PROFILE_NAMES = (AP_PROFILE_NAME, "Hotspot", "TickerSetup-AP")
|
||||
|
||||
# LED status message file (for display_controller integration)
|
||||
LED_STATUS_FILE = None # Will be set dynamically
|
||||
# LED status message file (for display_controller integration). None means
|
||||
# each WiFiManager writes next to its own config file, which for the default
|
||||
# config is get_wifi_status_path(), the file the display reads. Set only to
|
||||
# redirect it (tests).
|
||||
LED_STATUS_FILE = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -131,12 +132,12 @@ class WiFiManager:
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._load_config()
|
||||
|
||||
# Set LED status file path (for display_controller integration)
|
||||
global LED_STATUS_FILE
|
||||
if LED_STATUS_FILE is None:
|
||||
project_root = self.config_path.parent.parent
|
||||
LED_STATUS_FILE = project_root / "config" / "wifi_status.json"
|
||||
|
||||
# LED status file (for display_controller integration): next to this
|
||||
# manager's config. It used to be a module global set by whichever
|
||||
# manager was built first, so a later one with a different config
|
||||
# path wrote its messages to the first one's directory.
|
||||
self._led_status_file = self.config_path.parent / "wifi_status.json"
|
||||
|
||||
# Check which tools are available
|
||||
self.has_nmcli = self._check_command("nmcli")
|
||||
self.has_iwlist = self._check_command("iwlist")
|
||||
@@ -182,21 +183,22 @@ class WiFiManager:
|
||||
duration: How long to show message (seconds)
|
||||
"""
|
||||
try:
|
||||
if LED_STATUS_FILE is None:
|
||||
status_file = LED_STATUS_FILE or getattr(self, '_led_status_file', None)
|
||||
if status_file is None:
|
||||
return
|
||||
|
||||
|
||||
status = {
|
||||
'message': message,
|
||||
'timestamp': time.time(),
|
||||
'duration': duration
|
||||
}
|
||||
LED_STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Write-then-rename: the display reads this at ~1 Hz and deletes
|
||||
# a file it can't parse, so a half-written one would lose the message.
|
||||
tmp_path = LED_STATUS_FILE.with_name(LED_STATUS_FILE.name + '.tmp')
|
||||
tmp_path = status_file.with_name(status_file.name + '.tmp')
|
||||
with open(tmp_path, 'w') as f:
|
||||
json.dump(status, f)
|
||||
os.replace(tmp_path, LED_STATUS_FILE)
|
||||
os.replace(tmp_path, status_file)
|
||||
logger.info(f"LED message: {message}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not write LED status message: {e}")
|
||||
@@ -204,8 +206,9 @@ class WiFiManager:
|
||||
def _clear_led_message(self):
|
||||
"""Clear any WiFi status message from LED display."""
|
||||
try:
|
||||
if LED_STATUS_FILE and LED_STATUS_FILE.exists():
|
||||
LED_STATUS_FILE.unlink()
|
||||
status_file = LED_STATUS_FILE or getattr(self, '_led_status_file', None)
|
||||
if status_file and status_file.exists():
|
||||
status_file.unlink()
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not clear LED status message: {e}")
|
||||
|
||||
@@ -1668,8 +1671,9 @@ class WiFiManager:
|
||||
Disconnect from the current WiFi network
|
||||
|
||||
Args:
|
||||
skip_ap_check: If True, skip auto-enabling AP mode after disconnect
|
||||
(useful when switching networks)
|
||||
skip_ap_check: Accepted for callers that still pass it; ignored.
|
||||
Auto-enabling AP mode after a disconnect is the
|
||||
wifi monitor daemon's job.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
@@ -1705,18 +1709,13 @@ class WiFiManager:
|
||||
logger.info("Successfully disconnected from WiFi network")
|
||||
# Wait longer for the disconnect to fully complete
|
||||
time.sleep(2)
|
||||
|
||||
# Check if AP mode should be auto-enabled
|
||||
# Skip if we're switching networks (skip_ap_check=True)
|
||||
if not skip_ap_check:
|
||||
auto_enable = self.config.get("auto_enable_ap_mode", True)
|
||||
if auto_enable:
|
||||
# Give it a moment, then check if we should enable AP mode
|
||||
time.sleep(1)
|
||||
self.check_and_manage_ap_mode()
|
||||
else:
|
||||
logger.debug("Skipping AP mode check (network switch in progress)")
|
||||
|
||||
|
||||
# No AP-mode check here. It used to run one, but the web
|
||||
# routes build a fresh WiFiManager per request, so its
|
||||
# grace-period counter started at 0 and one check could
|
||||
# never reach the 3 needed to enable the AP: it only added
|
||||
# seconds of sleeps and nmcli calls. The monitor daemon's
|
||||
# long-lived manager is what counts consecutive checks.
|
||||
return True, "Disconnected from WiFi network"
|
||||
else:
|
||||
error_msg = result.stderr.strip() or result.stdout.strip()
|
||||
|
||||
Reference in New Issue
Block a user