First hardware test of node-26#9/#10 found three blockers:
- antirez/dump1090 has no --write-json, so ADS-B mode exited on start.
Swapped to wiedehopf/readsb; map alt_baro/gs (old names as fallback).
- op25 is not always on RTL-SDR index 0 (radio-box: op25 on 1, 0 free).
start() now tries each index and keeps the first decoder that stays up.
AIS-catcher index flag fixed to -d:N ("-d N" selects by serial).
- status() reported "running" for a decoder that died on startup: the
zombie still answered killpg(pgid, 0). Liveness now via Popen.poll().
install.sh blacklists dvb_usb_rtl28xxu, which claimed the second dongle.
Verified on radio-box: 831 msgs/min, 6 aircraft (3 with position) on a
9cm whip; op25 unaffected. AIS still untested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirmed prod bug: §5 keyed idempotency on configs/credentials.json only.
Re-running the installer on a pending-then-approved node (the exact flow the
script's own output tells you to do) found no credentials.json and fell
through to a fresh POST /nodes/enroll — which enrollment.py's CRITICAL GUARD
answers with 403 for an already-approved node_id, rendered as "use Reissue
key". The working path (GET /nodes/{id}/credentials with the saved
pickup_secret — no already-approved guard on that endpoint) was only ever
tried within a single run.
§5 rewritten as a decision tree that runs before any POST /nodes/enroll:
- credentials.json has api_key -> skip (unchanged)
- configs/pickup_secret exists -> GET /credentials with it:
200 + api_key -> write credentials.json, done
200, no key -> say "approve it, re-run"; clean exit, start stack
401 (rotated) -> re-enroll iff --token, else specific die
404 (deleted) -> re-enroll iff --token, else specific die
000 -> connectivity die
- no creds, no pickup_secret -> fresh enroll (do_fresh_enroll)
Also: fresh-enroll 403/401/429 handlers are now specific and point at
pickup-secret recovery, not just "Reissue key"; --wait-approval polling now
applies on the re-run path; §7's "re-run the installer" banner is
conditional on pickup_secret existing.
Response shapes verified against enrollment.py @ v1. approve_node mints
node_keys/{id}.api_key synchronously — no second bug; assigning a system is
independent and not required for a key. bash -n clean.
Recovery for a node stuck by the old behaviour (approved, no credentials.json,
pickup_secret on disk): re-run the patched install.sh (no --token needed), or
curl -fsS $C2_URL/nodes/$NODE_ID/credentials \
-H "X-Pickup-Secret: $(cat configs/pickup_secret)" | jq '{api_key}' > configs/credentials.json
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
install.sh is the `curl -fsSL <url> | sudo bash -s -- --token ...` bootstrap:
preflight (root/arch/apt/SDR) → install docker + compose + git + jq → clone
node-26 at a pinned ref (default `v1`, `--track-main` opt-in) → write .env
non-interactively from flags/env with a /dev/tty interactive fallback →
enroll with C2 (POST /nodes/enroll, poll GET /nodes/{id}/credentials, matches
drb-c2-core/app/routers/enrollment.py exactly) and write configs/credentials.json
→ docker compose pull && up -d (prebuilt; --build opts into the ~1h op25 build)
→ print the admin-approval step. Idempotent: re-run picks up the api_key after
approval; existing .env is preserved.
- setup.sh deleted — two scripts writing .env drift. install.sh owns it now.
- Makefile `setup:` no longer calls the removed script (cp .env.example fallback).
- README Setup section rewritten around the one-liner; `make setup`/`make up`
kept as the local-dev path.
Notes carried in the script header: git.vpn.cusano.net is public (D1, settled);
`v1` must be re-cut at this change's merge commit so the tag actually contains
install.sh. Standing hazard D3: docker-compose.yml bind-mounts the app source
over the image, so a pinned ref and the pulled image tags must not diverge.
Client-side enrollment still belongs in the edge-node app (mqtt_manager.py:73-85);
install.sh doing it is the interim. Tracked for follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>