I3 (per ADR-099). Three changes in main.rs:
1) AppStateInner: + intro: IntrospectionState + intro_tx: broadcast::Sender<String>
(256-slot ring, same shape as the existing tx).
2) ESP32 frame path: after the global frame_history push, before the
per-node mutable borrow of s.node_states, compute the per-frame derived
feature (mean amplitude across subcarriers), call s.intro.update(ts_ns,
feature), and broadcast the snapshot JSON to s.intro_tx. Placement is
deliberate — between the global state's mutable touch and the per-node
&mut so borrow-checking stays linear; ns is borrowed *after* the tap
completes its s.intro / s.intro_tx access.
3) Routes:
ws_introspection_handler → /ws/introspection
api_introspection_snapshot → /api/v1/introspection/snapshot
Same Axum + tokio::sync::broadcast pattern as ws_sensing_handler,
subscribed against s.intro_tx. Wrapped by the bearer-auth middleware
already on /api/v1/* — orchestrator probes and unauthenticated /ws/sensing
reachers continue to land on the existing topic.
Verified:
cargo build -p wifi-densepose-sensing-server --no-default-features ✓
cargo test -p wifi-densepose-sensing-server --no-default-features
lib: 207 passed, 0 failed (199 pre-tap + 8 introspection)
integration suites: 70, 8, 16, 18 passed, 0 failed
cargo clippy: clean on the introspection surface (pre-existing warnings
on -core / -ruvector / -signal unchanged).
Co-Authored-By: claude-flow <ruv@ruv.net>