Browser sign-in worked, but the settings panel kept offering "Sign in with
Cognitum" afterwards; only a hard reload showed the true state. The server was
correct throughout.
Root cause: `ui/sw.js` routed cache-first as its CATCH-ALL for every path
outside `/api/` and `/health/`. `/oauth/status` therefore had its first
(signed-out) response stored in the Cache API and replayed to the page forever.
The Cache API is not the HTTP cache and ignores `Cache-Control` entirely, so the
`no-store, no-cache, must-revalidate` the server already sends could not prevent
it. A hard reload bypasses the service worker, which is why that alone appeared
to fix it, and why signing out re-poisoned the entry.
Fixes, in order of blast radius:
- `/oauth/` is never handled by the worker. Not network-first — that still
writes a copy, which would be replayed the moment the server is briefly
unreachable, silently reinstating a stale sign-in state.
- Cache-first is now an ALLOWLIST (navigations and static asset extensions)
rather than the catch-all. This is the underlying defect: any endpoint added
outside `/api/` was frozen on its first response. Unrecognised paths now go
to the network untouched.
- `CACHE_NAME` bumped to `ruview-v2`, so `activate` evicts the poisoned
`ruview-v1` from browsers that already ran the old worker. Verified: the
cache list went from `[ruview-v1]` to `[ruview-v2]` on update.
- Shell lookups use `ignoreSearch` and store a search-less key, so the
`?signed_in=<ms>` the callback redirects to does not mint a fresh, never-hit
cache entry per sign-in.
Also: re-check sign-in state on `pageshow` (bfcache restore, where no script
re-runs) and on `visibilitychange` (signed in or out in another tab). Opening
the panel alone is not sufficient — it may already be open.
Verification, in a real browser driven end-to-end:
- Reproduced with a valid session cookie: server returned `signed_in: true` to
curl while the page's own fetch got a cached `signed_in: false`, and the
request never appeared in the server log at all.
- Confirmed the cached entry existed: `caches.open('ruview-v1').match(
'/oauth/status')` returned the signed-out body.
- After the fix, on a NORMAL (not hard) reload the panel reads
"Signed in as <account> - sensing:read" with Sign out shown.
Tests: `ui/sw.test.mjs` loads the real `sw.js` with stubbed worker globals and
asserts the routing decision per path. 4 of its 10 tests fail against the
pre-fix worker and pass after; the other 6 pin pre-existing guards (non-GET,
websocket upgrade, cross-origin, API paths, static assets, navigation) and pass
in both, so they track behaviour rather than the rewrite.
CI: adds a `ui-tests` job. Nothing ran the UI JavaScript before, so both this
suite and the ADR-272 ws-ticket suite would have rotted unexecuted — which is
the same blind spot that let this defect ship.
Removes the temporary `/oauth/status` cookie logging and the panel console
diagnostic added while tracking this down.
Co-Authored-By: Ruflo & AQE
Three loose ends from the browser sign-in work.
--- 1. The last mile: a button ---
The server endpoints existed but nothing in ui/ linked to them, so the feature
was unreachable. QuickSettings gains a "Cognitum Account" panel that renders
from `GET /oauth/status` and offers Sign in / Sign out.
`/oauth/status` is a new endpoint and is deliberately UNGATED — a signed-OUT
browser cannot ask a gated API whether sign-in is available. It returns only
capability flags and, when a session exists, who it belongs to. Never a
credential.
The panel distinguishes four states rather than showing a button that might
404: signed in; sign-in available; auth on but OAuth off (points at the
existing static-token panel); no auth required. A 404 from /oauth/status means
a server predating this work and says so plainly.
Sign-in is a full-page navigation, not fetch(): the server replies 302 and the
browser must follow it carrying the transaction cookie. An XHR would follow the
redirect invisibly and land nowhere.
--- 2. RUVIEW_SESSION_SECRET no longer required ---
Previously `/oauth/start` returned 503 unless an operator invented a secret —
a footgun, since they set RUVIEW_OAUTH_ISSUER, expect sign-in, and get a 503
naming an env var they have never heard of.
Now resolved in order: env var, then `<data_dir>/session-secret`, then generate
one and persist it 0600 (temp file, chmod before rename — the discipline used
for the CLI's credentials). Persisted rather than in-memory so a restart does
not silently sign everyone out.
The env var still wins, which is what a multi-instance deployment needs: several
servers must share a secret or a session issued by one is rejected by the next.
If the file cannot be written we log the reason and continue with an in-memory
secret rather than refusing sign-in outright.
Verified with NO configuration at all: secret generated, file mode 0600,
/oauth/start returns 302, /oauth/status reports browser_signin: true.
--- 3. The JavaScript is now executed by tests ---
`ui/services/ws-ticket.test.mjs`, 9 tests, node:test built-in — no new
dependency and no package.json needed. Run: `node --test ui/services/`.
Covers: no token means no fetch and an unchanged URL; the bearer reaches the
Authorization header and NEVER the URL; `?` vs `&` when a query already exists;
ticket URL-encoding; 404 treated as a pre-ADR-272 server (the property that
lets one UI work against old and new servers, and therefore lets the legacy
escape hatch be removed later); 503 and network failure swallowed rather than
breaking the connect path; and that a fresh ticket is minted per call, since
tickets are single-use and caching one fails on the second reconnect.
STATED PRECISELY, because the distinction matters: this EXECUTES the module in
Node with stubbed fetch/localStorage. It is more than the `node --check` it
replaces and less than a browser — no real WebSocket upgrade, no real cookies,
no page wiring. "The UI JavaScript has never been run" is no longer true of
this module. "Browser-tested" still is not.
Tests: 547 sensing-server lib, 5 wiring integration, 87 ruview-auth, 9 JS.
Co-Authored-By: Ruflo & AQE
Completes ADR-272. Three parts.
1. PREFIX MATCHING, not an allowlist. Anything under `/ws/` is a WebSocket path,
plus `/api/v1/stream/pose` which lives outside it. An allowlist means every
WebSocket route added later ships ungated until someone remembers to extend
it — the same bug reintroduced on a delay. Not hypothetical:
`/ws/train/progress` (ADR-186, arriving with PR #1387) is ALREADY referenced
by ui/services/training.service.js and would have shipped unauthenticated.
Pinned by a test that asserts it is gated before it exists.
2. UI wiring. A shared `withWsTicket()` helper mints a ticket immediately before
each connection attempt — never cached, because a ticket is single-use and
expires in seconds, so reusing one across reconnects fails on the second
attempt. Wired into the three sites that open gated sockets:
sensing.service.js, websocket-client.js, observatory/js/main.js.
It degrades in both directions on purpose: no stored token means auth is off
and no ticket is needed; a 404 from /api/v1/ws-ticket means a server
predating this ADR, which still exempts WebSockets, so connecting without a
ticket is correct there. The same UI therefore works against old and new
servers, which is what makes the escape hatch removable later rather than
permanent.
The long-lived bearer token is still never put in a URL — only the ticket is.
3. ADR-272 itself. Previously cited in five places without existing; the same
dangling-reference mistake made with ADR-271 earlier today, so it is written
before this lands rather than after someone notices.
It records the measured before/after, why a credential in a URL is
acceptable here specifically (single use, seconds-long, not the credential),
why the escape hatch exists and why it is deliberately uncomfortable, and
what is deliberately NOT done — including that /health/metrics stays ungated,
with the caveat that this should be revisited if metrics ever carry
occupancy-derived values, since that would make them sensing data wearing an
ops label.
Tests: 526 sensing-server (4 new path-matching), 82 ruview-auth. JS
syntax-checked with `node --check`; there is no UI test suite to extend.
Co-Authored-By: Ruflo & AQE
- sdkconfig.defaults.devkitc: header build command idf v5.2 -> v5.4
(source needs esp_driver_uart, IDF >=5.3 — same reason the PR fixed
the README refs)
- engine/lib.rs: separate doc comments — set_room_adapter's ADR-150
adapter/witness doc had merged into set_multistatic_config's doc
- ui: apply stored bearer token at api.service.js module load instead
of QuickSettings.init — services + dashboard tab dispatch their first
/api/v1/* requests before initializeEnhancements() runs, so the first
requests on every load went out without the Authorization header
- CHANGELOG: Unreleased entries for #1308/#1309/#1310
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ui): unbreak viz.html — OrbitControls importmap, WS URL, toast NPE (#760)
Three independent bugs were stacking to make ui/viz.html unusable from `main`:
1. Three.js r160 removed `examples/js/OrbitControls.js`, so the script-tag
load 404'd and `new THREE.OrbitControls(...)` threw. Switch to an
importmap that pulls the ES module build, then re-expose
`window.THREE` and `THREE.OrbitControls` so the existing component
modules (scene.js, body-model.js, …) keep working without a wider
refactor.
2. The WebSocket client was hardcoded to `ws://localhost:8000/ws/pose`,
but the sensing-server listens on `--ws-port` (8765 default, 3001 in
the Docker image) at `/ws/sensing`. Reuse the existing
`buildSensingWsUrl()` helper from `sensing.service.js` so port
pairings are handled centrally, and add a `?ws=…` query-string
override for non-standard setups. The websocket-client.js default is
also updated to derive from `window.location` instead of the dead
`:8000/ws/pose` literal.
3. `ToastManager.show()` called `this.container.appendChild(...)` even
when `init()` had never been called, throwing a TypeError that
killed the rest of page initialization. Auto-init the container
lazily on first show (patch from issue reporter).
Closes#760.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ui): single module script + mutable THREE — OrbitControls validated
Browser validation against the previous commit caught two stacked issues:
1. `import * as THREE from 'three'` returns a frozen Module Namespace
Object — assignment `THREE.OrbitControls = OrbitControls` silently
no-ops, so the global never gets the OrbitControls reference.
2. Two separate `<script type="module">` blocks (one installing the
THREE global, one consuming it via Scene) are independently
async-resolved. The second can finish dependency loading first and
call `new THREE.OrbitControls(...)` before the first script has run.
Fixed by spreading the namespace into a plain mutable object and merging
all initialization into a single module script with `await import()` for
component modules. Order is now strictly: import THREE → install
window.THREE → import components → run init().
Validated via agent-browser: page logs `[VIZ] Initialization complete`,
WebSocket targets the correct `ws://127.0.0.1:3001/ws/sensing` endpoint
(derived from buildSensingWsUrl), toast lazy-init confirmed via eval.
Co-Authored-By: claude-flow <ruv@ruv.net>
Complements #326 (per-node state pipeline) with additional features:
- Dynamic adaptive classifier: discover activity classes from training
data filenames instead of hardcoded array. Users add classes via
filename convention (train_<class>_<desc>.jsonl), no code changes.
- Per-node UI cards: SensingTab shows individual node status with
color-coded markers, RSSI, variance, and classification per node.
- Colored node markers in 3D gaussian splat view (8-color palette).
- Per-node RSSI history tracking in sensing service.
- XSS fix: UI uses createElement/textContent instead of innerHTML.
- RSSI sign fix: ensure dBm values are always negative.
- GET /api/v1/nodes endpoint for per-node health monitoring.
- node_features field in WebSocket SensingUpdate messages.
- Firmware watchdog fix: yield after every frame to prevent IDLE1 starvation.
Addresses #237, #276, #282
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The web UI had persistent 404 errors on model, recording, and training
endpoints, and the sensing WebSocket never connected on Dashboard/Live
Demo tabs because sensingService.start() was only called lazily on
Sensing tab visit.
Server (main.rs):
- Add 14 fully-functional Axum handlers: model CRUD (7), recording
lifecycle (4), training control (3)
- Scan data/models/ and data/recordings/ at startup
- Recording writes CSI frames to .jsonl via tokio background task
- Model load/unload lifecycle with state tracking
Web UI (app.js):
- Import and start sensingService early in initializeServices() so
Dashboard and Live Demo tabs connect to /ws/sensing immediately
Mobile (ws.service.ts):
- Fix WebSocket URL builder to use same-origin port instead of
hardcoded port 3001
Mobile (jest.config.js):
- Fix testPathIgnorePatterns that was ignoring the entire test directory
Mobile (25 test files):
- Replace all it.todo() placeholder tests with real implementations
covering components, services, stores, hooks, screens, and utils
ADR-043 documents all changes.
* feat: RVF training pipeline & UI integration (ADR-036)
Implement full model training, management, and inference pipeline:
Backend (Rust):
- recording.rs: CSI recording API (start/stop/list/download/delete)
- model_manager.rs: RVF model loading, LoRA profile switching, model library
- training_api.rs: Training API with WebSocket progress streaming, simulated
training mode with realistic loss curves, auto-RVF export on completion
- main.rs: Wire new modules, recording hooks in all CSI paths, data dirs
UI (new components):
- ModelPanel.js: Dark-mode model library with load/unload, LoRA dropdown
- TrainingPanel.js: Recording controls, training config, live Canvas charts
- model.service.js: Model REST API client with events
- training.service.js: Training + recording API client with WebSocket progress
UI (enhancements):
- LiveDemoTab: Model selector, LoRA profile switcher, A/B split view toggle,
training quick-panel with 60s recording shortcut
- SettingsPanel: Full dark mode conversion (issue #92), model configuration
(device, threads, auto-load), training configuration (epochs, LR, patience)
- PoseDetectionCanvas: 10-frame pose trail with ghost keypoints and motion
trajectory lines, cyan trail toggle button
- pose.service.js: Model-inference confidence thresholds
UI (plumbing):
- index.html: Training tab (8th tab)
- app.js: Panel initialization and tab routing
- style.css: ~250 lines of training/model panel dark-mode styles
191 Rust tests pass, 0 failures. Closes#92.
Refs: ADR-036, #93
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix: real RuVector training pipeline + UI service fixes
Training pipeline (training_api.rs):
- Replace simulated training with real signal-based training loop
- Load actual CSI data from .csi.jsonl recordings or live frame history
- Extract 180 features per frame: subcarrier amplitudes, temporal variance,
Goertzel frequency analysis (9 bands), motion gradients, global stats
- Train calibrated linear CSI-to-pose mapping via mini-batch gradient descent
with L2 regularization (ridge regression), Xavier init, cosine LR decay
- Self-supervised: teacher targets from derive_pose_from_sensing() heuristics
- Real validation metrics: MSE and PCK@0.2 on 80/20 train/val split
- Export trained .rvf with real weights, feature normalization stats, witness
- Add infer_pose_from_model() for live inference from trained model
- 16 new tests covering features, training, inference, serialization
UI fixes:
- Fix double-URL bug in model.service.js and training.service.js
(buildApiUrl was called twice — once in service, once in apiService)
- Fix route paths to match Rust backend (/api/v1/train/*, /api/v1/recording/*)
- Fix request body formats (session_name, nested config object)
- Fix top-level await in LiveDemoTab.js blocking module graph
- Dynamic imports for ModelPanel/TrainingPanel in app.js
- Center nav tabs with flex-wrap for 8-tab layout
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix: WebSocket onOpen race condition, data source indicators, auto-start pose detection
- Fix WebSocket onOpen race condition in websocket.service.js where
setupEventHandlers replaced onopen after socket was already open,
preventing pose service from receiving connection signal
- Add 4-state data source indicator (LIVE/SIMULATED/RECONNECTING/OFFLINE)
across Dashboard, Sensing, and Live Demo tabs via sensing.service.js
- Add hot-plug ESP32 auto-detection in sensing server (auto mode runs
both UDP listener and simulation, switches on ESP32_TIMEOUT)
- Auto-start pose detection when backend is reachable
- Hide duplicate PoseDetectionCanvas controls when enableControls=false
- Add standalone Demo button in LiveDemoTab for offline animated demo
- Add data source banner and status styling
Co-Authored-By: claude-flow <ruv@ruv.net>
- Convert all Live Demo sidebar panels to dark theme matching rest of UI
- Fix pose_source not reaching LiveDemoTab (was lost in
convertZoneDataToRestFormat — now passes through from WS message)
- Dark backgrounds, muted text, consistent border opacity throughout
- Estimation Mode badge colors adjusted for dark background contrast
Co-Authored-By: claude-flow <ruv@ruv.net>
- Docker default changed from --source simulated to --source auto
(auto-detects ESP32 on UDP 5005, falls back to simulation)
- Pose derivation now driven by real sensing features: motion_band_power,
breathing_band_power, variance, dominant_freq_hz, change_points
- Temporal feature extraction: 100-frame circular buffer, Goertzel
breathing rate estimation (0.1-0.5 Hz), frame-to-frame L2 motion
detection, SNR-based signal quality metric
- Signal field driven by subcarrier variance spatial mapping instead
of fixed animation circle
- UI data source indicators: LIVE/RECONNECTING/SIMULATED banner on
sensing tab, estimation mode badge on live demo tab
- Setup guide panel explaining ESP32 count requirements for each
capability level (1x: presence, 3x: localization, 4x+: full pose)
- Tick rate improved from 500ms to 100ms (2fps to 10fps)
- Fixed Option<f64> division bug from PR #83
- ADR-035 documents all decisions
Closes#86
Co-Authored-By: claude-flow <ruv@ruv.net>
The UI had hardcoded localhost:8080 for HTTP and localhost:8765 for
WebSocket, causing "Backend unavailable" when served from Docker
(port 3000) or any non-default port.
Changes:
- api.config.js: BASE_URL now uses window.location.origin instead
of hardcoded localhost:8080
- api.config.js: buildWsUrl() uses window.location.host instead of
hardcoded localhost:8080
- sensing.service.js: WebSocket URL derived from page origin instead
of hardcoded localhost:8765
- main.rs: Added /ws/sensing route to the HTTP server so WebSocket
and REST are reachable on a single port
Fixes#55
Co-Authored-By: claude-flow <ruv@ruv.net>
Replace Python FastAPI + WebSocket servers with a single 2.1MB Rust binary
(wifi-densepose-sensing-server) that serves all UI endpoints:
- REST: /health/*, /api/v1/info, /api/v1/pose/current, /api/v1/pose/stats,
/api/v1/pose/zones/summary, /api/v1/stream/status
- WebSocket: /api/v1/stream/pose (pose_data with 17 COCO keypoints),
/ws/sensing (raw sensing_update stream on port 8765)
- Static: /ui/* with no-cache headers
WiFi-derived pose estimation: derive_pose_from_sensing() generates 17 COCO
keypoints from CSI/WiFi signal data with motion-driven animation.
Data sources: ESP32 CSI via UDP :5005, Windows WiFi via netsh, simulation
fallback. Auto-detection probes each in order.
UI changes:
- Point all endpoints to Rust server on :8080 (was Python :8000)
- Fix WebSocket sensing URL to include /ws/sensing path
- Remove sensingOnlyMode gating — all tabs init normally
- Remove api.service.js sensing-only short-circuit
- Fix clearPingInterval bug in websocket.service.js
Also removes obsolete k8s/ template manifests.
Co-Authored-By: claude-flow <ruv@ruv.net>
- Add Python WebSocket sensing server (ws_server.py) with ESP32 UDP CSI
and Windows RSSI auto-detect collectors on port 8765
- Add Three.js Gaussian splat renderer with custom GLSL shaders for
real-time WiFi signal field visualization (blue→green→red gradient)
- Add SensingTab component with RSSI sparkline, feature meters, and
motion classification badge
- Add sensing.service.js WebSocket client with reconnect and simulation fallback
- Implement sensing-only mode: suppress all DensePose API calls when
FastAPI backend (port 8000) is not running, clean console output
- ADR-019: Document sensing-only UI architecture and data flow
- ADR-020: Migrate AI/model inference to Rust with RuVector ONNX Runtime,
replacing ~2.7GB Python stack with ~50MB static binary
- Add ruvnet/ruvector as upstream remote for RuVector crate ecosystem
Co-Authored-By: claude-flow <ruv@ruv.net>
Add viz.html as the main entry point that loads Three.js from CDN and
orchestrates all visualization components (scene, body model, signal
viz, environment, HUD). Add data-processor.js that transforms API
WebSocket messages into geometry updates and provides demo mode with
pre-recorded pose cycling when the server is unavailable.
https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
Add prominent hardware requirements table at top of README documenting
the three paths to real CSI data (ESP32, research NIC, commodity WiFi).
Include remaining Three.js visualization components for dashboard.
https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714