# Startup Environment Check + Self-Heal Date: 2026-08-19 ## Problem Rebooting the Pineapple left Mark VIII in a broken state: the boot-persistent procd service came back, but nothing reconciled pineapd/UCI/monitors, so the UI reported state that did not match the device (pool "off" while actually broadcasting), native recon scans failed, and scans only returned 5GHz results. Three observed failures: 1. **native recon scan failed** — `h_recon_start` proxies `/api/pineap/recon/new`; it returns 502 when the daemon fails (typically pineapd down/crash-looping). Nothing verifies pineapd health before the user starts a scan. 2. **scans only showed 5GHz** — radio0's AP interfaces (wlan0open/wlan0wpa) pin `wlan0mon` to one 2.4GHz channel (a phy's channel is held by its AP interface), so 2.4GHz results mostly vanish while 5GHz `wlan1mon` hopping keeps producing results. The band UCI config itself is correct. 3. **pool "on" while UI shows "off"** — the UI's advertise state is derived only from UCI `pineapd.@ssidpool[0].disable`. Nothing syncs pineapd's *runtime* pool broadcast to match UCI. UCI can say disabled while pineapd is live-broadcasting. ## Design decisions - Keep boot auto-start; the service self-heals at every startup instead. - The env check auto-fixes everything fixable, re-verifies, and only fails hard on core deps (daemon unreachable after fix, pineapd down after restart, recon DB unreadable). - 5GHz-only scans are handled by verify + warn (no scan-time AP pausing). ## Implementation ### 1. `env_check()` in server.py (single source of truth) Runs in order and returns `[{step, ok, detail, action}]` where `ok` is one of `pass`, `fixed`, `warn`, `fail`: 1. **Daemon reachable** — `daemon_sock_call('GET', '/api/pineap/get_config')`. Report-only. 2. **pineapd alive** — `pidof pineapd`. If down: stabilize UCI, restart `/etc/init.d/pineapd`, re-verify. Core: if still down after restart -> fail. 3. **Sane-off UCI defaults** — reuse the `_stabilize_pineapd` wanted-dict (pool `disable=1` + clear pool list, `wlan2mon.disable=1`/`hop=0`, `wlan1mon.bands=5`/`hop=0`, `wlan0mon.bands=2`). Write + commit when missing. Refactor so the health monitor and env check share the wanted-dict. 4. **Runtime pool sync** — when UCI says pool disabled, call `_pineap('SSIDPOOL', 'DISABLE')` so pineapd's runtime broadcast matches the UI. Report the resulting runtime state as `pool_runtime`. 5. **Monitors up** — `_bring_monitors_up()` for wlan0mon/wlan1mon. 6. **Recon DB readable** — read-only scan-count query against `RECON_DB`. Report row count. Core: unreadable -> fail. 7. **2.4GHz sampling** — report enabled radio0 APs (wlan0open/wlan0wpa). Warn: "2.4GHz under-sampled while an OpenAP/Evil WPA AP is up on radio0". ### 2. CLI mode `server.py --env-check` `if __name__ == '__main__'` branch: run `env_check()`, print verbose `[PASS] step — detail` / `[FIXED] ...` / `[WARN] ...` / `[FAIL] ...` lines to stdout, exit 0 (all pass/warn) or 1 (any core fail). ### 3. Startup self-heal `serve()` runs `env_check()` before binding, stores the report in module state, and logs to `/tmp/pagerwebui.log` (covers boot + procd respawn). ### 4. API + UI - `GET /api/health` gains `env` (last env-check report + timestamp + pass/fixed/warn/fail counts) and `pool_runtime` (actual runtime broadcast state after sync). - `GET /api/recon/status` gains `wlan0_pinned` (true when a radio0 AP is enabled); the recon scan bar shows a 2.4GHz under-sampling warning pill. - `h_recon_start` failure includes `detail` in the user-facing error. ### 5. payload.sh Before starting the service (both foreground and background paths), run `python3 "$SCRIPT_DIR/server.py" --env-check`, show output verbosely on the payload screen, and abort with a red message on exit 1. Bump header version to match the current release. ### 6. Tests - `tests/test_env_check.py` — fake `device_run` UCI harness (pattern from `test_health.py`): defaults applied when missing / idempotent when set, pool list cleared, runtime SSIDPOOL disable invoked when UCI says disabled, monitors brought up, pineapd restarted when down, DB unreadable -> fail, CLI `--env-check` prints and exits correctly. - `test_health.py` updated for the shared stabilization refactor. ## Out of scope - No scan-time AP pausing / radio0 AP teardown. - No changes to radio1 / evil-twin / enterprise config handling.