fix: non-disruptive startup checks + clean service lifecycle (v1.3.1)

- Env check is read-only when state is sane: no pineapd command-socket
  writes, no live pool-list commits, no wifi reload; pineapd restarts only
  when a runtime-sensitive UCI value changed or the daemon was down
- Failed monitor repairs now fail the startup contract instead of being
  reported as fixed; runtime pool state is read from active config
- Enterprise AP recovery runs only on device boot (PAGER_WEBUI_BOOT), not
  on every web-service restart
- serve() gates the HTTP port on startup checks with bounded retries and
  shuts down cleanly on SIGTERM/SIGINT; the recon watchdog waits
  interruptibly
- Recon uses a bounded userspace channel scheduler that drives both
  monitor radios over non-DFS channels, with preflight verification,
  serialized starts, and per-cycle error reporting
- payload.sh waits for real readiness on start, fully removes the boot
  service (stop + disable + delete) on stop, and surfaces a
  stopped-but-enabled boot service; deploy.sh refreshes the installed
  init script even when the service is stopped
- Bump version to 1.3.1

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
2026-08-19 12:03:11 -05:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent 6e3968c19a
commit cd26553d21
8 changed files with 451 additions and 68 deletions
+6
View File
@@ -14,6 +14,7 @@ class HealthCheckTest(unittest.TestCase):
def setUp(self):
self.runs = []
self.ping_ok = True
self.ip_link_ok = True
self.sigsegvs = 0
self.iface_up = {'wlan0mon': True, 'wlan1mon': True}
self.uci_state = {}
@@ -32,6 +33,11 @@ class HealthCheckTest(unittest.TestCase):
return (1, '', '')
if a[0] == 'logread':
return (0, 'SIGSEGV\n' * self.sigsegs if hasattr(self, 'sigsegs') else '', '')
if a[:3] == ['ip', 'link', 'set']:
if self.ip_link_ok:
self.iface_up[a[3]] = True
return (0, '', '')
return (1, '', 'interface unavailable')
if a[:2] == ['uci', 'set']:
k, _, v = a[2].partition('=')
self.uci_state[k] = v