mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-19 11:08:39 +00:00
Compare commits
9 Commits
baebe4f5f7
...
fix/post-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae95a1015 | ||
|
|
246ea54635 | ||
|
|
a0f957be9e | ||
|
|
76cd010aab | ||
|
|
587daa780e | ||
|
|
c19df29a21 | ||
|
|
b361866679 | ||
|
|
ceb4c4105f | ||
|
|
e9af18cdf1 |
@@ -1,4 +1,5 @@
|
|||||||
# LEDMatrix
|
# LEDMatrix
|
||||||
|
[](https://app.codacy.com/gh/ChuckBuilds/LEDMatrix/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||||
## Welcome to LEDMatrix!
|
## Welcome to LEDMatrix!
|
||||||
Welcome to the LEDMatrix Project! This open-source project enables you to run an information-rich display on a Raspberry Pi connected to an LED RGB Matrix panel. Whether you want to see your calendar, weather forecasts, sports scores, stock prices, or any other information at a glance, LEDMatrix brings it all together.
|
Welcome to the LEDMatrix Project! This open-source project enables you to run an information-rich display on a Raspberry Pi connected to an LED RGB Matrix panel. Whether you want to see your calendar, weather forecasts, sports scores, stock prices, or any other information at a glance, LEDMatrix brings it all together.
|
||||||
|
|
||||||
|
|||||||
@@ -1086,6 +1086,7 @@ SYSTEMCTL_PATH=$(which systemctl)
|
|||||||
REBOOT_PATH=$(which reboot)
|
REBOOT_PATH=$(which reboot)
|
||||||
POWEROFF_PATH=$(which poweroff)
|
POWEROFF_PATH=$(which poweroff)
|
||||||
BASH_PATH=$(which bash)
|
BASH_PATH=$(which bash)
|
||||||
|
JOURNALCTL_PATH=$(which journalctl 2>/dev/null || true)
|
||||||
|
|
||||||
# Create sudoers content
|
# Create sudoers content
|
||||||
cat > /tmp/ledmatrix_web_sudoers << EOF
|
cat > /tmp/ledmatrix_web_sudoers << EOF
|
||||||
@@ -1101,10 +1102,23 @@ $ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix.service
|
|||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH enable ledmatrix.service
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH enable ledmatrix.service
|
||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH disable ledmatrix.service
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH disable ledmatrix.service
|
||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix.service
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix-web.service
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix-web.service
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix-web.service
|
||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $PYTHON_PATH $PROJECT_ROOT_DIR/display_controller.py
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $PYTHON_PATH $PROJECT_ROOT_DIR/display_controller.py
|
||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT_DIR/start_display.sh
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT_DIR/start_display.sh
|
||||||
$ACTUAL_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT_DIR/stop_display.sh
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT_DIR/stop_display.sh
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_ROOT_DIR/scripts/fix_perms/safe_plugin_rm.sh *
|
||||||
EOF
|
EOF
|
||||||
|
if [ -n "$JOURNALCTL_PATH" ]; then
|
||||||
|
cat >> /tmp/ledmatrix_web_sudoers << EOF
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $JOURNALCTL_PATH -u ledmatrix.service *
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $JOURNALCTL_PATH -u ledmatrix *
|
||||||
|
$ACTUAL_USER ALL=(ALL) NOPASSWD: $JOURNALCTL_PATH -t ledmatrix *
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "$SUDOERS_FILE" ] && cmp -s /tmp/ledmatrix_web_sudoers "$SUDOERS_FILE"; then
|
if [ -f "$SUDOERS_FILE" ] && cmp -s /tmp/ledmatrix_web_sudoers "$SUDOERS_FILE"; then
|
||||||
echo "Sudoers configuration already up to date"
|
echo "Sudoers configuration already up to date"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
requests>=2.28.0
|
requests>=2.28.0
|
||||||
Pillow>=9.1.0
|
Pillow>=12.2.0
|
||||||
pytz>=2022.1
|
pytz>=2022.1
|
||||||
numpy>=1.24.0
|
numpy>=1.24.0
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ TEMP_SUDOERS="/tmp/ledmatrix_web_sudoers_$$"
|
|||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service"
|
||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix"
|
||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix.service"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH is-active ledmatrix.service"
|
||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix-web"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix-web.service"
|
||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix-web"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix-web.service"
|
||||||
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix-web"
|
echo "$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix-web.service"
|
||||||
|
|
||||||
# Optional: journalctl (non-critical — skip if not found)
|
# Optional: journalctl (non-critical — skip if not found)
|
||||||
if [ -n "$JOURNALCTL_PATH" ]; then
|
if [ -n "$JOURNALCTL_PATH" ]; then
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add project root to path (parent of scripts/utils/)
|
# Add project root to path (parent of scripts/utils/)
|
||||||
@@ -132,7 +133,7 @@ class WiFiMonitorDaemon:
|
|||||||
# AP-enable trigger clean and avoid false-positive AP enables from
|
# AP-enable trigger clean and avoid false-positive AP enables from
|
||||||
# transient packet loss on otherwise working WiFi.
|
# transient packet loss on otherwise working WiFi.
|
||||||
if updated_status.connected and not updated_status.ap_mode_active:
|
if updated_status.connected and not updated_status.ap_mode_active:
|
||||||
if not self.wifi_manager._check_internet_connectivity():
|
if not self.wifi_manager.check_internet_connectivity():
|
||||||
self._consecutive_internet_failures += 1
|
self._consecutive_internet_failures += 1
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Internet unreachable despite nmcli connection "
|
f"Internet unreachable despite nmcli connection "
|
||||||
@@ -140,10 +141,24 @@ class WiFiMonitorDaemon:
|
|||||||
)
|
)
|
||||||
if self._consecutive_internet_failures >= self._nm_restart_threshold:
|
if self._consecutive_internet_failures >= self._nm_restart_threshold:
|
||||||
logger.warning("Restarting NetworkManager to recover internet connectivity")
|
logger.warning("Restarting NetworkManager to recover internet connectivity")
|
||||||
import subprocess as _sp
|
try:
|
||||||
_sp.run(["sudo", "systemctl", "restart", "NetworkManager"],
|
subprocess.run(
|
||||||
capture_output=True, timeout=20)
|
["/usr/bin/systemctl", "restart", "NetworkManager"],
|
||||||
self._consecutive_internet_failures = 0
|
capture_output=True, timeout=20, check=True
|
||||||
|
)
|
||||||
|
self._consecutive_internet_failures = 0
|
||||||
|
# NM restart causes a brief WiFi drop; reset the AP-mode grace
|
||||||
|
# counter so that transient disconnect doesn't count toward
|
||||||
|
# triggering AP mode.
|
||||||
|
self.wifi_manager._disconnected_checks = 0
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"NetworkManager restart failed (rc={e.returncode}); "
|
||||||
|
"resetting failure counter to avoid tight retry loop")
|
||||||
|
self._consecutive_internet_failures = 0
|
||||||
|
except (subprocess.SubprocessError, OSError) as e:
|
||||||
|
logger.error(f"NetworkManager restart error: {e}; "
|
||||||
|
"resetting failure counter to avoid tight retry loop")
|
||||||
|
self._consecutive_internet_failures = 0
|
||||||
else:
|
else:
|
||||||
self._consecutive_internet_failures = 0
|
self._consecutive_internet_failures = 0
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ class WiFiManager:
|
|||||||
|
|
||||||
# Timestamp set when AP mode is enabled; used for the idle-timeout check
|
# Timestamp set when AP mode is enabled; used for the idle-timeout check
|
||||||
self._ap_enabled_at: Optional[float] = None
|
self._ap_enabled_at: Optional[float] = None
|
||||||
|
# Which redirect backend was used (iptables/nftables/None); set per-instance
|
||||||
|
self._redirect_backend: Optional[str] = None
|
||||||
|
|
||||||
logger.info(f"WiFi Manager initialized - nmcli: {self.has_nmcli}, iwlist: {self.has_iwlist}, "
|
logger.info(f"WiFi Manager initialized - nmcli: {self.has_nmcli}, iwlist: {self.has_iwlist}, "
|
||||||
f"hostapd: {self.has_hostapd}, dnsmasq: {self.has_dnsmasq}, "
|
f"hostapd: {self.has_hostapd}, dnsmasq: {self.has_dnsmasq}, "
|
||||||
@@ -691,9 +693,8 @@ class WiFiManager:
|
|||||||
|
|
||||||
def _validate_ap_config(self) -> Tuple[str, int]:
|
def _validate_ap_config(self) -> Tuple[str, int]:
|
||||||
"""Return a sanitized (ssid, channel) pair from config, falling back to defaults."""
|
"""Return a sanitized (ssid, channel) pair from config, falling back to defaults."""
|
||||||
import re as _re
|
|
||||||
ssid = str(self.config.get("ap_ssid", DEFAULT_AP_SSID))
|
ssid = str(self.config.get("ap_ssid", DEFAULT_AP_SSID))
|
||||||
if not ssid or len(ssid) > 32 or not _re.match(r'^[\x20-\x7E]+$', ssid):
|
if not ssid or len(ssid) > 32 or not re.match(r'^[\x20-\x7E]+$', ssid):
|
||||||
logger.warning(f"AP SSID '{ssid}' is invalid, falling back to default")
|
logger.warning(f"AP SSID '{ssid}' is invalid, falling back to default")
|
||||||
ssid = DEFAULT_AP_SSID
|
ssid = DEFAULT_AP_SSID
|
||||||
try:
|
try:
|
||||||
@@ -705,10 +706,6 @@ class WiFiManager:
|
|||||||
channel = DEFAULT_AP_CHANNEL
|
channel = DEFAULT_AP_CHANNEL
|
||||||
return ssid, channel
|
return ssid, channel
|
||||||
|
|
||||||
# Tracks which redirect backend was used so teardown uses the same one.
|
|
||||||
# Value is "iptables", "nftables", or None (not set up).
|
|
||||||
_redirect_backend: Optional[str] = None
|
|
||||||
|
|
||||||
def _setup_iptables_redirect(self) -> bool:
|
def _setup_iptables_redirect(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Add port 80 → 5000 redirect rules for the captive portal.
|
Add port 80 → 5000 redirect rules for the captive portal.
|
||||||
@@ -936,18 +933,22 @@ class WiFiManager:
|
|||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
logger.debug("Internet connectivity confirmed via ping 8.8.8.8")
|
logger.debug("Internet connectivity confirmed via ping 8.8.8.8")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except (subprocess.SubprocessError, OSError):
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
import urllib.request as _ureq
|
import urllib.request as _ureq
|
||||||
_ureq.urlopen("http://connectivity-check.ubuntu.com/", timeout=timeout)
|
_ureq.urlopen("http://connectivity-check.ubuntu.com/", timeout=timeout)
|
||||||
logger.debug("Internet connectivity confirmed via HTTP check")
|
logger.debug("Internet connectivity confirmed via HTTP check")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
logger.debug("Internet connectivity check failed (both ping and HTTP)")
|
logger.debug("Internet connectivity check failed (both ping and HTTP)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_internet_connectivity(self, timeout: int = 5) -> bool:
|
||||||
|
"""Public wrapper around _check_internet_connectivity for use by the daemon."""
|
||||||
|
return self._check_internet_connectivity(timeout=timeout)
|
||||||
|
|
||||||
def _has_ap_clients(self) -> bool:
|
def _has_ap_clients(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if at least one client is associated with the AP.
|
Return True if at least one client is associated with the AP.
|
||||||
@@ -2070,6 +2071,7 @@ class WiFiManager:
|
|||||||
if up_result.returncode != 0:
|
if up_result.returncode != 0:
|
||||||
error_msg = up_result.stderr.strip() or up_result.stdout.strip()
|
error_msg = up_result.stderr.strip() or up_result.stdout.strip()
|
||||||
logger.error(f"Failed to bring up AP connection: {error_msg}")
|
logger.error(f"Failed to bring up AP connection: {error_msg}")
|
||||||
|
self._remove_nm_dnsmasq_captive_conf()
|
||||||
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
||||||
capture_output=True, timeout=10)
|
capture_output=True, timeout=10)
|
||||||
self._show_led_message("AP mode failed", duration=5)
|
self._show_led_message("AP mode failed", duration=5)
|
||||||
@@ -2081,6 +2083,7 @@ class WiFiManager:
|
|||||||
# need to add the iptables port-redirect rules for the captive portal.
|
# need to add the iptables port-redirect rules for the captive portal.
|
||||||
if not self._setup_iptables_redirect():
|
if not self._setup_iptables_redirect():
|
||||||
logger.error("Captive-portal redirect setup failed; rolling back AP profile")
|
logger.error("Captive-portal redirect setup failed; rolling back AP profile")
|
||||||
|
self._remove_nm_dnsmasq_captive_conf()
|
||||||
subprocess.run(["nmcli", "connection", "down", "LEDMatrix-Setup-AP"],
|
subprocess.run(["nmcli", "connection", "down", "LEDMatrix-Setup-AP"],
|
||||||
capture_output=True, timeout=10)
|
capture_output=True, timeout=10)
|
||||||
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
||||||
@@ -2098,6 +2101,7 @@ class WiFiManager:
|
|||||||
else:
|
else:
|
||||||
logger.error("AP mode started but not verified by status check — rolling back")
|
logger.error("AP mode started but not verified by status check — rolling back")
|
||||||
self._teardown_iptables_redirect()
|
self._teardown_iptables_redirect()
|
||||||
|
self._remove_nm_dnsmasq_captive_conf()
|
||||||
subprocess.run(["nmcli", "connection", "down", "LEDMatrix-Setup-AP"],
|
subprocess.run(["nmcli", "connection", "down", "LEDMatrix-Setup-AP"],
|
||||||
capture_output=True, timeout=10)
|
capture_output=True, timeout=10)
|
||||||
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
subprocess.run(["nmcli", "connection", "delete", "LEDMatrix-Setup-AP"],
|
||||||
@@ -2107,6 +2111,7 @@ class WiFiManager:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error starting AP mode with nmcli: {e}")
|
logger.error(f"Error starting AP mode with nmcli: {e}")
|
||||||
|
self._remove_nm_dnsmasq_captive_conf()
|
||||||
self._show_led_message("Setup mode error", duration=5)
|
self._show_led_message("Setup mode error", duration=5)
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
@@ -2494,7 +2499,10 @@ address=/detectportal.firefox.com/192.168.4.1
|
|||||||
# Idle-timeout check: disable AP if no client has connected within the window.
|
# Idle-timeout check: disable AP if no client has connected within the window.
|
||||||
# Only applies when AP is active and we haven't just decided to enable/disable it.
|
# Only applies when AP is active and we haven't just decided to enable/disable it.
|
||||||
if ap_active and self._ap_enabled_at is not None:
|
if ap_active and self._ap_enabled_at is not None:
|
||||||
idle_timeout_min = self.config.get("ap_idle_timeout_minutes", 15)
|
try:
|
||||||
|
idle_timeout_min = max(1, min(1440, int(self.config.get("ap_idle_timeout_minutes", 15))))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
idle_timeout_min = 15
|
||||||
elapsed = time.time() - self._ap_enabled_at
|
elapsed = time.time() - self._ap_enabled_at
|
||||||
if elapsed > idle_timeout_min * 60 and not self._has_ap_clients():
|
if elapsed > idle_timeout_min * 60 and not self._has_ap_clients():
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -129,7 +129,15 @@ def test_nmcli_ap_profile_has_no_security_params(manager: WiFiManager) -> None:
|
|||||||
assert "psk" not in add_str, "AP profile must not include a PSK/password"
|
assert "psk" not in add_str, "AP profile must not include a PSK/password"
|
||||||
assert "wpa" not in add_str.lower(), "AP profile must not reference WPA"
|
assert "wpa" not in add_str.lower(), "AP profile must not reference WPA"
|
||||||
assert "802-11-wireless.mode" in add_str, "AP profile must declare wireless mode"
|
assert "802-11-wireless.mode" in add_str, "AP profile must declare wireless mode"
|
||||||
assert "ap" in add_calls[0], "Wireless mode value must be 'ap'"
|
# Verify the value for 802-11-wireless.mode is exactly "ap" — check the element
|
||||||
|
# that immediately follows the key in the command list, not a loose substring match.
|
||||||
|
cmd = add_calls[0]
|
||||||
|
try:
|
||||||
|
mode_idx = cmd.index("802-11-wireless.mode")
|
||||||
|
assert cmd[mode_idx + 1] == "ap", \
|
||||||
|
f"802-11-wireless.mode value must be exactly 'ap', got {cmd[mode_idx + 1]!r}"
|
||||||
|
except ValueError:
|
||||||
|
pytest.fail("802-11-wireless.mode not found as a list element in nmcli command")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from flask import Blueprint, request, jsonify, Response, send_from_directory
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -16,6 +17,11 @@ from typing import Optional, Tuple, Dict, Any, Type
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUDO_BIN = shutil.which("sudo") or "/usr/bin/sudo"
|
||||||
|
SYSTEMCTL_BIN = shutil.which("systemctl") or "/usr/bin/systemctl"
|
||||||
|
REBOOT_BIN = shutil.which("reboot") or "/usr/sbin/reboot"
|
||||||
|
POWEROFF_BIN = shutil.which("poweroff") or "/usr/sbin/poweroff"
|
||||||
|
|
||||||
# Import new infrastructure
|
# Import new infrastructure
|
||||||
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
||||||
from src.web_interface.errors import ErrorCode
|
from src.web_interface.errors import ErrorCode
|
||||||
@@ -218,7 +224,7 @@ def _ensure_display_service_running():
|
|||||||
if status.get('active'):
|
if status.get('active'):
|
||||||
status['started'] = False
|
status['started'] = False
|
||||||
return status
|
return status
|
||||||
result = _run_systemctl_command(['sudo', 'systemctl', 'start', 'ledmatrix'])
|
result = _run_systemctl_command([SUDO_BIN, SYSTEMCTL_BIN, 'start', 'ledmatrix.service'])
|
||||||
service_status = _get_display_service_status()
|
service_status = _get_display_service_status()
|
||||||
result['started'] = result.get('returncode') == 0
|
result['started'] = result.get('returncode') == 0
|
||||||
result['active'] = service_status.get('active')
|
result['active'] = service_status.get('active')
|
||||||
@@ -227,7 +233,7 @@ def _ensure_display_service_running():
|
|||||||
|
|
||||||
def _stop_display_service():
|
def _stop_display_service():
|
||||||
"""Stop the ledmatrix display service."""
|
"""Stop the ledmatrix display service."""
|
||||||
result = _run_systemctl_command(['sudo', 'systemctl', 'stop', 'ledmatrix'])
|
result = _run_systemctl_command([SUDO_BIN, SYSTEMCTL_BIN, 'stop', 'ledmatrix.service'])
|
||||||
status = _get_display_service_status()
|
status = _get_display_service_status()
|
||||||
result['active'] = status.get('active')
|
result['active'] = status.get('active')
|
||||||
result['status'] = status
|
result['status'] = status
|
||||||
@@ -1716,33 +1722,34 @@ def execute_system_action():
|
|||||||
if mode:
|
if mode:
|
||||||
# For on-demand modes, we would need to integrate with the display controller
|
# For on-demand modes, we would need to integrate with the display controller
|
||||||
# For now, just start the display service
|
# For now, just start the display service
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'start', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success' if result.returncode == 0 else 'error',
|
'status': 'success' if result.returncode == 0 else 'error',
|
||||||
'message': f'Started display in {mode} mode',
|
'message': f'Started display in {mode} mode' if result.returncode == 0
|
||||||
|
else f'Failed to start display in {mode} mode: {result.stderr.strip() or "check sudo systemctl status ledmatrix.service"}',
|
||||||
'returncode': result.returncode,
|
'returncode': result.returncode,
|
||||||
'stdout': result.stdout,
|
'stdout': result.stdout,
|
||||||
'stderr': result.stderr
|
'stderr': result.stderr
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'start', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
elif action == 'stop_display':
|
elif action == 'stop_display':
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'stop', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'stop', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
elif action == 'enable_autostart':
|
elif action == 'enable_autostart':
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'enable', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'enable', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
elif action == 'disable_autostart':
|
elif action == 'disable_autostart':
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'disable', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'disable', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
elif action == 'reboot_system':
|
elif action == 'reboot_system':
|
||||||
result = subprocess.run(['sudo', 'reboot'],
|
result = subprocess.run([SUDO_BIN, REBOOT_BIN],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=10)
|
||||||
elif action == 'shutdown_system':
|
elif action == 'shutdown_system':
|
||||||
result = subprocess.run(['sudo', 'poweroff'],
|
result = subprocess.run([SUDO_BIN, POWEROFF_BIN],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=10)
|
||||||
elif action == 'git_pull':
|
elif action == 'git_pull':
|
||||||
# Use PROJECT_ROOT instead of hardcoded path
|
# Use PROJECT_ROOT instead of hardcoded path
|
||||||
project_dir = str(PROJECT_ROOT)
|
project_dir = str(PROJECT_ROOT)
|
||||||
@@ -1823,12 +1830,11 @@ def execute_system_action():
|
|||||||
'stderr': result.stderr
|
'stderr': result.stderr
|
||||||
})
|
})
|
||||||
elif action == 'restart_display_service':
|
elif action == 'restart_display_service':
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'restart', 'ledmatrix'],
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'restart', 'ledmatrix.service'],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True, timeout=15)
|
||||||
elif action == 'restart_web_service':
|
elif action == 'restart_web_service':
|
||||||
# Try to restart the web service (assuming it's ledmatrix-web.service)
|
result = subprocess.run([SUDO_BIN, SYSTEMCTL_BIN, 'restart', 'ledmatrix-web.service'],
|
||||||
result = subprocess.run(['sudo', 'systemctl', 'restart', 'ledmatrix-web'],
|
capture_output=True, text=True, timeout=15)
|
||||||
capture_output=True, text=True)
|
|
||||||
else:
|
else:
|
||||||
return jsonify({'status': 'error', 'message': f'Unknown action: {action}'}), 400
|
return jsonify({'status': 'error', 'message': f'Unknown action: {action}'}), 400
|
||||||
|
|
||||||
@@ -1840,6 +1846,13 @@ def execute_system_action():
|
|||||||
'stderr': result.stderr
|
'stderr': result.stderr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if action == 'start_display' and mode:
|
||||||
|
msg = f'Failed to start display in {mode} mode: timed out'
|
||||||
|
else:
|
||||||
|
msg = f'Action {action} timed out'
|
||||||
|
logger.warning("[System] execute_system_action timed out: action=%s", action)
|
||||||
|
return jsonify({'status': 'error', 'message': msg, 'returncode': -1, 'stdout': '', 'stderr': 'timeout'}), 500
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("[System] execute_system_action failed")
|
logger.exception("[System] execute_system_action failed")
|
||||||
return jsonify({'status': 'error', 'message': 'Failed to execute system action'}), 500
|
return jsonify({'status': 'error', 'message': 'Failed to execute system action'}), 500
|
||||||
@@ -7136,7 +7149,7 @@ def connect_wifi():
|
|||||||
# Propagate structured error type so the captive portal UI can show
|
# Propagate structured error type so the captive portal UI can show
|
||||||
# "Wrong password — try again" instead of a generic failure message.
|
# "Wrong password — try again" instead of a generic failure message.
|
||||||
error_type = "wrong_password" if (message or "").startswith("wrong_password:") else "connection_failed"
|
error_type = "wrong_password" if (message or "").startswith("wrong_password:") else "connection_failed"
|
||||||
clean_message = (message or "").removeprefix("wrong_password: ") or "Failed to connect to network"
|
clean_message = (message or "").removeprefix("wrong_password:").lstrip() or "Failed to connect to network"
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': clean_message,
|
'message': clean_message,
|
||||||
|
|||||||
@@ -1225,7 +1225,9 @@ function initializePlugins() {
|
|||||||
window.pluginManager._reswap = false;
|
window.pluginManager._reswap = false;
|
||||||
// Await the installed-plugins fetch so window.installedPlugins is populated before
|
// Await the installed-plugins fetch so window.installedPlugins is populated before
|
||||||
// searchPluginStore renders Installed/Reinstall badges against it.
|
// searchPluginStore renders Installed/Reinstall badges against it.
|
||||||
loadInstalledPlugins().then(() => {
|
loadInstalledPlugins().catch(err => {
|
||||||
|
console.error('[PluginStore] loadInstalledPlugins failed:', err);
|
||||||
|
}).finally(() => {
|
||||||
searchPluginStore(!isReswapWarm);
|
searchPluginStore(!isReswapWarm);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4851,9 +4853,9 @@ window.executePluginAction = function(actionId, actionIndex, pluginIdParam = nul
|
|||||||
showNotification(data.message || 'Action completed successfully!', 'success');
|
showNotification(data.message || 'Action completed successfully!', 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
statusDiv.innerHTML = `<div class="text-red-600"><i class="fas fa-exclamation-circle mr-2"></i>${data.message}</div>`;
|
statusDiv.innerHTML = `<div class="text-red-600"><i class="fas fa-exclamation-circle mr-2"></i>${escapeHtml(data.message || 'Error')}</div>`;
|
||||||
if (data.output) {
|
if (data.output) {
|
||||||
statusDiv.innerHTML += `<pre class="mt-2 text-xs bg-red-50 p-2 rounded overflow-auto max-h-32">${data.output}</pre>`;
|
statusDiv.innerHTML += `<pre class="mt-2 text-xs bg-red-50 p-2 rounded overflow-auto max-h-32">${escapeHtml(data.output)}</pre>`;
|
||||||
}
|
}
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -4897,8 +4899,8 @@ window.executePluginAction = function(actionId, actionIndex, pluginIdParam = nul
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="text-sm text-blue-700 mb-2">1. Click the link below to authorize:</p>
|
<p class="text-sm text-blue-700 mb-2">1. Click the link below to authorize:</p>
|
||||||
<a href="${data.auth_url}" target="_blank" class="text-blue-600 hover:text-blue-800 underline break-all">
|
<a href="${data.auth_url && data.auth_url.startsWith('http') ? escapeHtml(data.auth_url) : '#'}" target="_blank" class="text-blue-600 hover:text-blue-800 underline break-all">
|
||||||
${data.auth_url}
|
${escapeHtml(data.auth_url || '')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -4920,7 +4922,7 @@ window.executePluginAction = function(actionId, actionIndex, pluginIdParam = nul
|
|||||||
<div class="text-green-900 font-medium mb-2">
|
<div class="text-green-900 font-medium mb-2">
|
||||||
<i class="fas fa-check-circle mr-2"></i>${data.message || 'Action completed successfully'}
|
<i class="fas fa-check-circle mr-2"></i>${data.message || 'Action completed successfully'}
|
||||||
</div>
|
</div>
|
||||||
${data.output ? `<pre class="mt-2 text-xs bg-green-50 p-2 rounded overflow-auto max-h-32">${data.output}</pre>` : ''}
|
${data.output ? `<pre class="mt-2 text-xs bg-green-50 p-2 rounded overflow-auto max-h-32">${escapeHtml(data.output)}</pre>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
@@ -4933,9 +4935,9 @@ window.executePluginAction = function(actionId, actionIndex, pluginIdParam = nul
|
|||||||
statusDiv.innerHTML = `
|
statusDiv.innerHTML = `
|
||||||
<div class="bg-red-50 border border-red-200 rounded p-3">
|
<div class="bg-red-50 border border-red-200 rounded p-3">
|
||||||
<div class="text-red-900 font-medium mb-2">
|
<div class="text-red-900 font-medium mb-2">
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>${data.message || 'Action failed'}
|
<i class="fas fa-exclamation-circle mr-2"></i>${escapeHtml(data.message || 'Action failed')}
|
||||||
</div>
|
</div>
|
||||||
${data.output ? `<pre class="mt-2 text-xs bg-red-50 p-2 rounded overflow-auto max-h-32">${data.output}</pre>` : ''}
|
${data.output ? `<pre class="mt-2 text-xs bg-red-50 p-2 rounded overflow-auto max-h-32">${escapeHtml(data.output)}</pre>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
@@ -8065,4 +8067,3 @@ setTimeout(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user