Files
Mark-VIII/docs/superpowers/specs/2026-08-19-startup-env-check-design.md
T

4.4 KiB

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 failedh_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 reachabledaemon_sock_call('GET', '/api/pineap/get_config'). Report-only.
  2. pineapd alivepidof 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.