Closes the last HTTP gap — the appliance can now drive the ENTIRE calibration
pipeline over HTTP without the CLI:
baseline (start/stop) -> enroll/anchor x8 -> room/train -> room/state
- POST /enroll/anchor { room_id, baseline, label, duration_s? }: the ingest task
loads the baseline (sanitized name under output_dir), captures the anchor for
the duration against it (AnchorRecorder + per-frame series), runs the quality
gate, and on completion replies with the verdict + accumulates the AnchorFeature
in an in-server enrollment map keyed by room_id. Re-prompts on rejection.
- GET /enroll/status?room=<id>: accepted anchors, next, complete.
- POST /room/train now falls back to the in-server enrollment when anchors[] is
omitted.
Validated live (ESP32-S3): capture baseline -> enroll stand_still (271 frames,
6s) -> gate correctly rejects "no person detected (presence_z 0.90 < 1.50)"
relative to a same-occupancy baseline (a clean empty-room baseline is the
documented on-target prerequisite). Builds clean; CLI tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
- POST /api/v1/room/train: { room_id, baseline_id, anchors[] } → trains a
SpecialistBank and persists it as <output_dir>/<room_id>.json (path-sanitized),
readable via /room/state?bank=<room_id>. Completes the HTTP train→infer loop.
- Fix data-contract bug: AnchorLabel serialized as PascalCase variant names
(serde default) while as_str() + the integration doc used snake_case. Added
#[serde(rename_all = "snake_case")] so the JSON wire format matches the
documented contract (empty/stand_still/…). Locked with a roundtrip test.
Validated live (ESP32-S3): POST train (4 anchors → 6 specialists, persisted) →
GET /room/state returns RoomState with the trained presence/restlessness; the
synthetic-vs-real scale mismatch correctly triggers the anomaly veto. 36
calibration tests pass.
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds a live RoomState readout over HTTP — the appliance UI's main need. The
ingest task maintains a rolling per-frame scalar window (flushed on the 200 ms
tick, no per-frame lock); the handler loads a bank (resolved as a sanitized
name under output_dir — same path-traversal defense as room_id), runs the
MixtureOfSpecialists over the window, returns RoomState JSON.
Validated live (ESP32-S3 via relay): breathing 14-19 BPM over HTTP; a
bank=../../etc/passwd query is neutralized to 'etcpasswd' (no traversal).
Co-Authored-By: claude-flow <ruv@ruv.net>
Aligns ADR-151 §7 + the appliance integration doc with the PR #989 scope
clarification: nothing has run a clean baseline → enroll → train → infer on
live CSI; the live breathing read used the stateless head, not a trained bank.
Adds --source-format adr018v6 to the backlog.
Co-Authored-By: claude-flow <ruv@ruv.net>
Resolves the review on #989:
- **Cross-compile (the appliance blocker):** make wifi-densepose-mat optional
and feature-gate it (`mat`), so `cargo build -p wifi-densepose-cli
--no-default-features` excludes the mat→nn→ort(ONNX)→openssl-sys chain.
Verified: `cargo tree --no-default-features` shows 0 ort/openssl deps →
calibration cross-compiles clean for the Pi.
- **Security (must-fix before LAN):**
- `--token` / CALIBRATE_TOKEN bearer-auth middleware on every route; warns if
bound non-loopback without a token.
- sanitize client-supplied `room_id` to [A-Za-z0-9_-] (≤64) before it reaches
the baseline write path — kills the `../` file-write primitive. + test.
- **Perf:** stop locking shared status + cloning SessionStatus on every UDP
frame — counters/snapshot flush on the 200 ms tick instead (no CPU
starvation under flood). finalize write moved to async `tokio::fs::write`.
- **Docs:** ADR-151 STALE wording matches the impl (baseline-id change;
drift-threshold = P6 refinement); integration doc gets the
`--no-default-features` build + auth/sanitize notes.
35 calibration + 15 CLI tests (no-default) / 20 CLI (default) pass.
Co-Authored-By: claude-flow <ruv@ruv.net>