The previous commit introduced _check_internet_connectivity() into
check_and_manage_ap_mode(), which shared the same _disconnected_checks counter
that triggers AP enable. This created a false-positive risk: 90 seconds of
packet loss on working WiFi would enable AP mode and kick off the connection.
Fix: restore nmcli association state as the sole AP-enable trigger (original,
safe behaviour). The internet connectivity check is now used only in the daemon
watchdog for the NM-restart escalation — matching how adsb-feeder-image actually
structures the two concerns (initial setup detection vs. ongoing monitoring).
Also clarify daemon comment: the connectivity check runs once per cycle in the
watchdog block, not inside check_and_manage_ap_mode, so there is no double-call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inspired by the production-proven approach in dirkhh/adsb-feeder-image.
1. DNS spoofing for automatic captive-portal popup (Change 1 — Critical)
Write /etc/NetworkManager/dnsmasq-shared.d/ledmatrix-captive.conf with
address=/#/192.168.4.1 before nmcli connection up so NM's built-in
dnsmasq (ipv4.method=shared) resolves every hostname to the AP IP.
This triggers the OS captive-portal popup automatically on iOS / Android /
Windows / macOS — no manual navigation to 192.168.4.1:5000/setup required.
New helpers: _write_nm_dnsmasq_captive_conf / _remove_nm_dnsmasq_captive_conf.
New constants: NM_DNSMASQ_SHARED_DIR / NM_DNSMASQ_SHARED_CONF.
2. Real internet connectivity check (Change 2 — High)
Add _check_internet_connectivity() (ping 8.8.8.8 + HTTP fallback).
check_and_manage_ap_mode() now considers a device "disconnected" when nmcli
shows connected but no real internet reachability, matching adsb-feeder's
multi-method gateway/DNS/HTTP test approach.
3. AP idle timeout (Change 3 — Medium)
Track _ap_enabled_at timestamp in enable_ap_mode(). Add _has_ap_clients()
using 'iw dev <iface> station dump'. check_and_manage_ap_mode() auto-disables
AP after ap_idle_timeout_minutes (default 15) with no associated clients.
4. Wrong-password error feedback (Change 4 — Medium)
_connect_nmcli() detects "Secrets were required" / "authentication rejected"
in nmcli stderr and prefixes the message with "wrong_password: ".
The /api/v3/wifi/connect route propagates error_type="wrong_password" in the
JSON response. captive_setup.html shows "Incorrect password — try again"
(keeping the form active) instead of the generic failure message.
5. Escalating watchdog NM restart (Change 5 — Low)
wifi_monitor_daemon.py tracks _consecutive_internet_failures. After
_nm_restart_threshold (5) consecutive checks where nmcli shows connected but
internet is unreachable, restart NetworkManager as a recovery step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Six pytest unit tests covering the five review scenarios. All subprocess and
filesystem side-effects are mocked so the tests run without root, hardware, or
a Pi OS environment.
1. test_nmcli_ap_profile_has_no_security_params — asserts the nmcli connection
add command has no key-mgmt / psk / WPA arguments and sets mode=ap.
2. test_iptables_nat_rules_added_on_ap_start — verifies _setup_iptables_redirect
emits a PREROUTING REDIRECT 80→5000 rule and an INPUT ACCEPT rule for port
5000 (not 80, which never hits INPUT after PREROUTING rewrites it).
3. test_iptables_rules_and_ip_forward_reverted_on_teardown — verifies the -D
PREROUTING/-D INPUT calls and that sysctl restores the saved ip_forward value
and removes the save file.
4. test_ip_forward_not_restored_when_save_file_absent — verifies teardown skips
sysctl when the save file was never written, preventing blind ip_forward=0 on
systems using ip_forward for VPNs or NM shared mode.
5. test_led_message_shows_ssid_no_password_and_url — asserts the LED message
includes the SSID, 'No password', and the 192.168.4.1:5000 setup URL.
6. test_existing_ap_profiles_deleted_before_new_profile_created — asserts all
known profile names are targeted for deletion before 'nmcli connection add'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Both AP startup paths (hostapd and nmcli) now check the bool returned by
_setup_iptables_redirect() and treat False as a hard failure: the hostapd
path stops hostapd/dnsmasq and returns an error tuple; the nmcli path brings
down and deletes the LEDMatrix-Setup-AP profile and clears the LED message
- _enable_ap_mode_hostapd's LED message now calls _validate_ap_config() to get
the same sanitized SSID that _create_hostapd_config() uses, so the displayed
name always matches the AP actually broadcast by hostapd
- _setup_iptables_redirect's outer except block now calls
_teardown_iptables_redirect() before returning False so partial iptables/
ip_forward state is always cleaned up on unexpected exceptions; cleanup
exceptions are caught and logged separately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _find_command_path() helper that extends _check_command()'s sbin-aware lookup to
return the absolute binary path rather than a boolean. Use it in
_setup_iptables_redirect and _teardown_iptables_redirect so iptables and sysctl are
resolved via /sbin or /usr/sbin even when those directories are absent from PATH in
systemd service environments.
Also harden the ip_forward save/restore logic:
- Read ip_forward from /proc/sys/net/ipv4/ip_forward (no subprocess, no PATH
dependency) instead of spawning sysctl -n
- Skip the sysctl -w ip_forward=1 write when the value is already "1" to avoid
mutating state owned by another service (VPN, NM shared mode, bridge)
- Track save success via presence of the save file: if the /proc read or file write
fails, leave the file absent so teardown knows not to restore
- In _teardown_iptables_redirect, only restore ip_forward when the save file exists;
if absent, leave the current value untouched rather than forcing "0"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wifi_manager.py:
- _create_hostapd_config: use _validate_ap_config() for ssid/channel instead
of raw self.config values; strip newlines from SSID to prevent config-file
injection via the generated hostapd.conf
- _setup_iptables_redirect: check return codes of sysctl ip_forward enable and
both iptables -A calls; on any failure log the error output, call
_teardown_iptables_redirect() to restore state, and return False instead of
silently succeeding
- _enable_ap_mode_nmcli_hotspot: on AP verification failure roll back fully —
tear down iptables redirect, delete the LEDMatrix-Setup-AP connection profile,
clear the LED message — before returning False
plugins_manager.js:
- initializePlugins: chain searchPluginStore(!isReswapWarm) inside
loadInstalledPlugins().then() so window.installedPlugins is populated before
the store renders Installed/Reinstall badges (same pattern applied to
refreshPlugins() in the previous commit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refreshPlugins() called searchPluginStore(true) and showNotification() immediately
after refreshInstalledPlugins() without awaiting the returned Promise, so
window.installedPlugins could still be stale when the store rendered its
Installed/Reinstall badges. Chain .then() so both run only after the fetch
completes.
In initializePlugins(), the re-swap path always passed fetchCommitInfo=false to
searchPluginStore, skipping GitHub metadata even when the 5-minute cache TTL had
expired. Add storeCacheExpired() helper and compute isReswapWarm = _reswap &&
!storeCacheExpired() so fresh metadata is fetched whenever the cache is cold,
regardless of whether the render is a first load or a tab re-swap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Validate ap_ssid/ap_channel from config before passing to subprocess
(printable ASCII ≤32 chars; channel 1-14) to prevent command injection
- Fix INPUT iptables rule: PREROUTING redirects port 80→5000 so the INPUT
chain sees dport=5000, not 80. Old INPUT rule on port 80 was a no-op.
- Refactor iptables setup/teardown into _setup_iptables_redirect() and
_teardown_iptables_redirect() helpers, eliminating duplicate logic in
the hostapd and nmcli paths
- Save/restore ip_forward state (via /tmp/ledmatrix_ip_forward_saved)
instead of forcing it to 0 on cleanup, which could break VPNs or
bridges already relying on forwarding
- nmcli path skips ip_forward management entirely: NM's ipv4.method=shared
already manages it for the duration of the connection
- Fix _get_ap_status_nmcli() verification: new 'connection add type wifi'
profiles have type '802-11-wireless', not 'hotspot', so verification was
always returning False. Now also matches by our known connection name.
- Remove SSID-based connection deletion: deleting any profile whose SSID
matched the AP SSID could destroy a user's saved home WiFi profile.
Now only deletes by our application-managed profile names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nmcli device wifi hotspot always attaches a WPA2 PSK on Bookworm/Trixie
and silently ignores post-creation security modifications, causing users
to be prompted for an unknown password. Switch to nmcli connection add
with 802-11-wireless.mode ap and no security section — NM cannot auto-add
a password to a profile that has no 802-11-wireless-security block.
Also:
- Remove dead DEFAULT_AP_PASSWORD / ap_password config field (stored but
never passed to hostapd or nmcli, causing user confusion)
- Add iptables port 80→5000 redirect to the nmcli AP path so captive portal
auto-popup works on phones without hostapd (previously only worked on
the hostapd path)
- Clean up iptables rules on disable for the nmcli path
- Improve LED message on AP enable: show SSID, "No password", and IP:port
on both paths so users know exactly how to connect
- Fix systemd template: replace hardcoded /home/ledpi/LEDMatrix/ with
__PROJECT_ROOT_DIR__ placeholder (install script already writes correct path)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>