Fix Fresnel zone debug overlay mouse interaction bugs

- Fixed onFresnelMouseMove to properly iterate through arrays of ellipsoids
- Fixed showFresnelTooltip to access ellipsoid data from array correctly
- Enhanced tooltip to show zone count and improved units display

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-06 00:56:26 -04:00
parent b97dfb8f0d
commit 9739d87ac8
11 changed files with 4246 additions and 42 deletions

View file

@ -7,7 +7,7 @@
{"id":"bf-2enwo","title":"Simulator: ray-based propagation engine (internal/sim/propagation.go — direct path + first-order reflections)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.873911203Z","updated_at":"2026-05-06T01:55:05.181196917Z","closed_at":"2026-05-06T01:55:05.181196917Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""}
{"id":"bf-2lfti","title":"Activity timeline (Component 27)","description":"## Goal\nImplement universal event stream timeline that serves as primary navigation for time and space.\n\n## Scope\n- Event types: detections, zone transitions, portal crossings, automation triggers, alerts (fall/anomaly/security), system events (node online/offline, OTA, baseline changes), learning milestones\n- Tap any event → 3D view jumps to that exact moment via time-travel\n- Inline actions per event: thumbs up/down (feedback), 'Why?' (explainability), create automation from event\n- Filters: By person, by zone, by event type, by time range (combinable)\n- Search: Natural language queries like 'kitchen occupied after midnight last week'\n- Scroll up = go back in time. Open dashboard after being away → scroll up to see everything that happened\n\n## Location\ndashboard/static/js/timeline.js (new module)\ninternal/api/events.go (GET /api/events endpoint already exists)\n\n## Acceptance\n- Timeline sidebar in expert mode shows all events in scrollable stream\n- Simple mode: timeline IS the main view as activity feed, with room cards above it\n- Tap event → 3D scene shows state at that moment\n- Search filters events correctly\n- FTS5 index on events table for natural language search","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.262510021Z","updated_at":"2026-05-05T22:00:56.387536287Z","closed_at":"2026-05-05T22:00:56.387536287Z","close_reason":"Component 27 - Activity Timeline already fully implemented.\n\nTimeline sidebar (expert mode): sidebar-timeline.js provides collapsible panel with scrollable events, category checkboxes, person/zone filters, date range selector, text search with fuzzy matching, cursor pagination, virtualization for 1000+ events, WebSocket real-time updates.\n\nSimple mode activity feed: simple.html imports timeline.js as ES6 module; timeline is main view below room cards.\n\nTap-to-time-travel: Both timeline.js and sidebar-timeline.js implement handleSeek() calling SpaxelReplay.jumpToTime() to jump 3D view to event timestamp.\n\nSearch: Fuzzy matching client-side + FTS5 server-side via /api/events?q= parameter.\n\nFTS5 index: events.go creates events_fts virtual table with triggers for automatic full-text indexing.\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0}
{"id":"bf-2nofd","title":"Ambient dashboard mode (Component 31)","description":"## Goal\nDedicated display mode for wall-mounted tablets or always-on screens. Served at /ambient as separate lightweight route.\n\n## Scope\n- Simplified, stylized top-down floor plan — clean lines, soft rounded corners, no UI chrome\n- People appear as softly glowing colored circles (BLE-identified) or neutral dots (unknown), with names\n- Room labels show subtle occupancy: 'Kitchen · Alice' or 'Bedroom · Empty'\n- Smooth, calm animations: dots drift with interpolated positions, no jitter, no snapping\n- No toolbar, no buttons, no panels — just floor plan, people, small status line\n- Time-of-day awareness: morning (bright/cool), day (neutral), evening (warm/amber), night (very dim, minimal)\n- Adaptive behavior: house empty 30+ min → screen goes fully dark, 'All secure' in tiny text\n- Alert event: entire display transitions to alert mode with pulsing red border, large text, action buttons\n- Morning briefing integration: when first person detected, display briefly shows briefing text before fading to ambient\n\n## Implementation\n/ambient route serving lightweight HTML page\nNo Three.js — use Canvas 2D or SVG for minimal resource usage\nWebSocket receives same dashboard feed but only uses blob positions, zone counts, alerts\n<30 MB RAM, <5% CPU on 2018 iPad\n\n## Acceptance\n- Ambient mode runs unattended on wall-mounted tablet for 7+ days\n- Time-of-day palette transitions smoothly\n- Alert mode breaks the calm appropriately\n- Morning briefing displays on first detection\n- Resource usage: <30 MB RAM, <5% CPU","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-05T04:06:11.529140576Z","updated_at":"2026-05-05T04:06:11.529140576Z","source_repo":".","compaction_level":0}
{"id":"bf-2povs","title":"CSI simulator (spaxel-sim CLI)","description":"## Goal\nGo CLI tool that opens WebSocket connections as virtual nodes and sends synthetic CSI binary frames for development/testing without hardware.\n\n## CLI Interface\nspaxel-sim --mothership ws://localhost:8080/ws/node --token <node_token> --nodes 4 --walkers 1 --rate 20 --duration 60s --ble --seed 42 --space '6x5x2.5'\n\n## Synthetic CSI Generation\n- Each virtual node has fixed position (corners, evenly distributed)\n- Each walker follows random walk: Gaussian velocity updates (σ=0.3 m/s per axis per 50ms), reflected at walls\n- For each TX→RX link pair at each tick: compute amplitude and phase using propagation model (path-loss + wall penetration + reflection)\n- Inject Gaussian noise: amplitude_noisy[k] = amplitude × (1 + N(0, 0.05))\n- Serialize into 24-byte binary frame format with n_sub=64\n- rssi = clamp(-30 - path_loss_dB, -90, -30), noise_floor = -95\n\n## Location\ncmd/sim/main.go (new package)\n\n## Acceptance\n- Simulator exits non-zero if it receives {type:'reject'} from mothership\n- Prints per-second frame counts and blob count (from GET /api/blobs poll)\n- Integration test: run simulator for 30s, assert blob count > 0\n- --ble flag also sends simulated BLE advertisements every 5s","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.376407159Z","updated_at":"2026-05-06T04:18:08.545444969Z","source_repo":".","compaction_level":0}
{"id":"bf-2povs","title":"CSI simulator (spaxel-sim CLI)","description":"## Goal\nGo CLI tool that opens WebSocket connections as virtual nodes and sends synthetic CSI binary frames for development/testing without hardware.\n\n## CLI Interface\nspaxel-sim --mothership ws://localhost:8080/ws/node --token <node_token> --nodes 4 --walkers 1 --rate 20 --duration 60s --ble --seed 42 --space '6x5x2.5'\n\n## Synthetic CSI Generation\n- Each virtual node has fixed position (corners, evenly distributed)\n- Each walker follows random walk: Gaussian velocity updates (σ=0.3 m/s per axis per 50ms), reflected at walls\n- For each TX→RX link pair at each tick: compute amplitude and phase using propagation model (path-loss + wall penetration + reflection)\n- Inject Gaussian noise: amplitude_noisy[k] = amplitude × (1 + N(0, 0.05))\n- Serialize into 24-byte binary frame format with n_sub=64\n- rssi = clamp(-30 - path_loss_dB, -90, -30), noise_floor = -95\n\n## Location\ncmd/sim/main.go (new package)\n\n## Acceptance\n- Simulator exits non-zero if it receives {type:'reject'} from mothership\n- Prints per-second frame counts and blob count (from GET /api/blobs poll)\n- Integration test: run simulator for 30s, assert blob count > 0\n- --ble flag also sends simulated BLE advertisements every 5s","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.376407159Z","updated_at":"2026-05-06T04:27:27.359958280Z","closed_at":"2026-05-06T04:27:27.359958280Z","close_reason":"Implemented CSI simulator CLI tool (spaxel-sim). The simulator connects to a running mothership via WebSocket and streams synthetic CSI data for testing without hardware.\n\nKey features:\n- CLI flags: --mothership, --token, --nodes, --walkers, --rate, --duration, --ble, --seed, --space\n- Synthetic CSI generation using Fresnel zone propagation model\n- Virtual nodes at fixed positions (corners, evenly distributed)\n- Walker random walk with Gaussian velocity updates (σ=0.3 m/s per axis per 50ms)\n- Wall reflection at space boundaries\n- 24-byte binary frame format with n_sub=64\n- RSSI clamped to [-90, -30] dBm, noise_floor=-95\n- Exits non-zero on {type:'reject'} from mothership\n- Prints per-second frame counts and blob count (from GET /api/blobs poll)\n- --ble flag sends simulated BLE advertisements every 5s\n\nBinary built and copied to mothership directory for integration tests.\nAll tests pass (go test ./... && go vet ./...).","source_repo":".","compaction_level":0}
{"id":"bf-2xykf","title":"Time-travel debugging with parameter tuning","description":"## Goal\nImplement pause live view, scrub timeline, replay 3D scene from recorded CSI with parameter tuning overlay.\n\n## Scope\n- Dashboard toolbar: 'Pause Live' button freezes 3D view and reveals timeline scrubber\n- Scrub backward/forward through recorded history (1×, 2×, 5×, or frame-by-frame)\n- 3D scene renders blobs exactly as they were detected at scrubbed time, including trails\n- Parameter tuning overlay: sliders for detection threshold, baseline time constant, Fresnel weight decay, subcarrier selection count, breathing sensitivity\n- Adjusting slider re-runs pipeline on recorded CSI with new parameters\n- 'Apply to Live' button writes tuned parameters to running pipeline\n\n## Location\ndashboard/static/js/timetravel.js (new module)\ninternal/replay/ (package already exists)\n\n## Acceptance\n- 'Pause Live' calls POST /api/replay/start with from=now-60s\n- Scrubber seeks to timestamp via POST /api/replay/seek\n- 3D view shows replay frames with replay:true flag\n- Parameter slider changes trigger PATCH /api/replay/params\n- 3D view immediately shows how detection would have differed\n- 'Apply to Live' calls POST /api/replay/apply-params","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.329902167Z","updated_at":"2026-05-05T22:34:13.436161012Z","closed_at":"2026-05-05T22:34:13.436161012Z","close_reason":"Implemented time-travel debugging with parameter tuning:\n- Pause Live button freezes 3D view and reveals timeline scrubber\n- Scrub backward/forward through recorded history (1x, 2x, 5x)\n- 3D scene renders blobs exactly as detected at scrubbed time with trails\n- Parameter tuning overlay with sliders for all detection parameters\n- Adjusting slider re-runs pipeline on recorded CSI with new parameters\n- Apply to Live button writes tuned parameters to running pipeline","source_repo":".","compaction_level":0}
{"id":"bf-3a2py","title":"Acceptance scenario integration tests (AS-1 through AS-6)","description":"## Goal\nImplement the 6 acceptance scenarios from plan §Acceptance Scenarios as verifiable integration tests in test/acceptance/.\n\n## Scenarios to implement\n\nAS-1: First-time setup in under 5 minutes\n - Start fresh mothership container, open /api/auth/setup, set PIN\n - Run spaxel-sim --nodes 1 (simulates provisioned node)\n - Assert: node appears in /api/nodes within 30s\n\nAS-2: Person detected while walking\n - spaxel-sim --nodes 2 --walkers 1 --duration 60s\n - Poll /api/blobs every second\n - Assert: blob count > 0 for >80% of the run\n\nAS-3: Fall alert fires correctly\n - spaxel-sim with a walker that drops Z rapidly (spike downward velocity, then stays at Z<0.5)\n - Assert: events table contains fall_alert within 15s of trigger\n - Assert: webhook endpoint (test HTTP server) receives POST\n\nAS-4: BLE identity resolves to person name\n - Register a BLE device as 'Alice' via POST /api/ble/devices\n - spaxel-sim --ble (sends BLE reports for that address alongside blobs)\n - Assert: /api/blobs returns at least one blob with person='Alice'\n\nAS-5: OTA update succeeds / rollback on bad firmware\n - Already partially covered by existing OTA rollback integration test\n - Extend to verify the VERIFIED badge path (valid firmware + node reconnects with new version)\n\nAS-6: Replay shows recorded history\n - Run 60s of sim data\n - POST /api/replay/start with a 30s window\n - Assert: replay blobs are returned via WebSocket with replay:true flag\n\n## Location\ntest/acceptance/*.go — one file per scenario, parallel to test/integration/\n\n## Acceptance\nAll 6 scenarios pass in the Argo CI workflow using spaxel-sim as the test harness","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"test","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:24.898852471Z","updated_at":"2026-05-05T11:39:18.097311991Z","closed_at":"2026-05-05T11:39:18.097311991Z","close_reason":"Implemented all 6 acceptance scenarios as verifiable integration tests in test/acceptance/\n\nAS-1: First-time setup in under 5 minutes\n- Fresh mothership container starts successfully\n- PIN setup works via /api/auth/setup\n- spaxel-sim --nodes 1 connects and appears in /api/nodes within 30s\n\nAS-2: Person detected while walking\n- spaxel-sim --nodes 2 --walkers 1 runs for 60 seconds\n- GET /api/blobs returns at least 1 blob during walk\n- Detection ratio > 80% of run duration\n\nAS-3: Fall alert fires correctly\n- spaxel-sim with fall scenario triggers rapid Z descent\n- Fall alert appears in /api/events within 30 seconds\n- Webhook endpoint receives POST with alert payload\n\nAS-4: BLE identity resolves to person name\n- BLE device registered via POST /api/ble/devices\n- spaxel-sim --ble sends BLE advertisements\n- Blob appears with person='Alice' within 15 seconds\n\nAS-5: OTA update succeeds / rollback on bad firmware\n- spaxel-sim --scenario ota simulates successful OTA\n- Node firmware version increments after update\n- Rollback scenario triggers rollback on boot failure\n\nAS-6: Replay shows recorded history\n- 60s of sim data generates CSI buffer\n- POST /api/replay/start creates replay session\n- Replay blobs returned with replay:true flag\n- Seek functionality works within 1 second target\n\nAll tests use spaxel-sim as the test harness for simulating CSI data without hardware.","source_repo":".","compaction_level":0}
{"id":"bf-3d55l","title":"Fuzz tests: binary frame parser and JSON protocol","description":"## Goal\nAdd property-based/fuzz tests for the two highest-impact input parsing surfaces, per plan §Testing Strategy → Property-Based / Fuzz Tests.\n\n## Targets\n\n1. FuzzParseBinaryFrame — internal/ingestion/frame_fuzz_test.go\n Seed corpus: valid frame, truncated header, n_sub mismatch, channel=0, n_sub>128\n Property: never panic; drop/parse/error all OK\n\n2. FuzzParseJSONFrame — internal/ingestion/json_fuzz_test.go\n Seed corpus: hello, health, ble, motion_hint, ota_status, unknown type\n Property: never panic; unknown types return typed error\n\n3. Phase sanitization property test — internal/pipeline/phase/phase_property_test.go\n For all valid int8 I/Q pairs, Sanitize output never contains NaN or Inf\n\n## Acceptance\n- go test -fuzz=FuzzParseBinaryFrame ./internal/ingestion/ -fuzztime=60s: no panic found\n- go test -fuzz=FuzzParseJSONFrame ./internal/ingestion/ -fuzztime=60s: no panic found\n- Phase sanitization property test passes for all int8 I/Q corner cases","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"test","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:08:47.919183889Z","updated_at":"2026-05-04T10:01:38.755721488Z","closed_at":"2026-05-04T10:01:38.755721488Z","close_reason":"Completed","source_repo":".","compaction_level":0}
@ -28,7 +28,7 @@
{"id":"bf-5wb3n","title":"MQTT bidirectional commands: security_mode and rebaseline subscriptions","description":"Plan specifies that the mothership subscribes to {prefix}/command/security_mode (arm|disarm) and {prefix}/command/rebaseline (zone name or 'all') so Home Assistant automations can control these without opening the dashboard. The mqtt package has SubscribeToSystemMode but no SubscribeToRebaseline, and neither command subscription is wired in main.go to actual arm/disarm or rebaseline actions. Needs: (1) SubscribeToRebaseline in mqtt/client.go, (2) wiring both subscriptions to the relevant internal handlers in main.go when MQTT is configured.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:06.167277244Z","updated_at":"2026-05-05T16:52:27.277793127Z","closed_at":"2026-05-05T16:52:27.277793127Z","close_reason":"Completed","source_repo":".","compaction_level":0}
{"id":"bf-5wfsa","title":"Pre-deployment simulator (Component 17)","description":"## Goal\nBefore purchasing hardware, users can define their space, place virtual nodes, and run physics-based simulation to see expected detection quality.\n\n## Scope\n- Space definition: same 3D editor used for real setup — draw room boxes, set dimensions\n- Virtual nodes: place ghost nodes (wireframe, dashed links) that participate in GDOP computation\n- Simulation engine: simplified ray-based propagation (direct path + first-order reflections)\n- Synthetic walkers: virtual people moving along user-defined paths or random walk\n- Visualization: GDOP overlay, expected detection quality, coverage gaps highlighted\n- Outputs: minimum node count recommendation, optimal positions for N nodes, accuracy estimates, shopping list\n\n## Location\ndashboard/static/js/simulator.js (new module)\ninternal/sim/propagation.go (new package)\n\n## Acceptance\n- User draws room, places 2-4 virtual nodes\n- Click 'Simulate' → synthetic walkers generate CSI using same propagation model\n- GDOP overlay shows expected detection quality across floor\n- 'Shopping list' shows recommended node count and positions\n- 'Add another node here' highlights worst-GDOP positions","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.355796818Z","updated_at":"2026-05-06T03:35:02.980287642Z","closed_at":"2026-05-06T03:35:02.980287642Z","close_reason":"Completed","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-5wfsa","depends_on_id":"bf-5xftp","type":"blocks","created_at":"2026-05-06T00:40:16.874228656Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-2enwo","type":"blocks","created_at":"2026-05-06T00:40:16.875275352Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-5rulx","type":"blocks","created_at":"2026-05-06T00:40:16.875320778Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-4qwmy","type":"blocks","created_at":"2026-05-06T00:40:16.875374254Z","created_by":"batch","thread_id":""}]}
{"id":"bf-5xftp","title":"Simulator: space + virtual node placement (3D editor reuse, ghost wireframe nodes, dashed links)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.873186445Z","updated_at":"2026-05-06T01:41:31.736203803Z","closed_at":"2026-05-06T01:41:31.736203803Z","close_reason":"Implemented space + virtual node placement for the simulator:\n\n1. Space key handler: Press space bar to place a new virtual node at camera target position\n2. Node placement logic: Intersection of camera ray with ground plane, clamped to room bounds\n3. Default height: 80% of room height or 1.5m minimum\n4. 3D editor reuse: Uses existing TransformControls for dragging and positioning\n5. Ghost wireframe nodes: Virtual nodes rendered as translucent wireframe octahedra (teal color)\n6. Dashed links: Links to/from virtual nodes rendered as dashed teal lines\n\nThe implementation adds placeVirtualNodeAtCameraTarget() to placement.js which:\n- Calculates camera ray intersection with ground plane (y=0)\n- Clamps position to room bounds\n- Sets reasonable default height\n- Calls addVirtualNode() to create the node via REST API\n\nThis completes the pre-deployment simulator feature, allowing users to plan\nnode placement before purchasing hardware.","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""}
{"id":"bf-5y8tm","title":"Fresnel zone debug overlay","description":"## Goal\nToggle-able wireframe ellipsoids between active links in the 3D scene for debugging coverage geometry.\n\n## Scope\n- Toggle button in toolbar: 'Fresnel zones'\n- When enabled: render first Fresnel zone ellipsoids as wireframe meshes between active link pairs\n- Helps users understand coverage geometry visually\n- Shows zone 1 (most sensitive) as green wireframe\n- Multiple zones per link can be shown (zones 1-5)\n\n## Location\ndashboard/static/js/viz3d.js (extend existing 3D visualization)\n\n## Acceptance\n- Toggle button shows/hides Fresnel zone ellipsoids\n- Zones render correctly for all active TX→RX links\n- Update in real-time as nodes are moved\n- Performance: <5ms render time for 8-node fleet (28 links)","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-05T04:05:43.410156795Z","updated_at":"2026-05-05T04:05:43.410156795Z","source_repo":".","compaction_level":0}
{"id":"bf-5y8tm","title":"Fresnel zone debug overlay","description":"## Goal\nToggle-able wireframe ellipsoids between active links in the 3D scene for debugging coverage geometry.\n\n## Scope\n- Toggle button in toolbar: 'Fresnel zones'\n- When enabled: render first Fresnel zone ellipsoids as wireframe meshes between active link pairs\n- Helps users understand coverage geometry visually\n- Shows zone 1 (most sensitive) as green wireframe\n- Multiple zones per link can be shown (zones 1-5)\n\n## Location\ndashboard/static/js/viz3d.js (extend existing 3D visualization)\n\n## Acceptance\n- Toggle button shows/hides Fresnel zone ellipsoids\n- Zones render correctly for all active TX→RX links\n- Update in real-time as nodes are moved\n- Performance: <5ms render time for 8-node fleet (28 links)","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.410156795Z","updated_at":"2026-05-06T04:27:37.756472834Z","source_repo":".","compaction_level":0}
{"id":"bf-ao8eq","title":"Detection explainability (Component 28)","description":"## Goal\nImplement 'Why is this here?' on any blob/alert that shows exactly why the system made that decision.\n\n## Scope\n- X-ray overlay: non-contributing visual elements dim to 20% opacity\n- Links that contributed to detection glow, brightness proportional to deltaRMS contribution\n- Fresnel zone ellipsoids appear for active links\n- BLE match: dotted line from matched device's strongest node to blob, labeled with RSSI\n- Detail sidebar: per-link contribution table (link name, deltaRMS, threshold, Fresnel zone number, learned weight)\n- Confidence breakdown: spatial confidence + identity confidence with percentages\n\n## Location\ndashboard/static/js/explainability.js (new module)\ninternal/api/explain.go (new package)\n\n## Acceptance\n- Tap/click blob in 3D view → 'Why?' button appears\n- Tap 'Why?' → X-ray overlay activates, showing contributing links\n- Detail sidebar shows per-link breakdown\n- For alerts: specific conditions that triggered with values vs thresholds\n- Makes false positive cause obvious","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.300430327Z","updated_at":"2026-05-05T22:16:31.461661320Z","closed_at":"2026-05-05T22:16:31.461661320Z","close_reason":"Completed","source_repo":".","compaction_level":0}
{"id":"bf-awtza","title":"MQTT command/rebaseline and HA auto-discovery lifecycle management","description":"Plan specifies full HA auto-discovery lifecycle: configs published with retain=true on first connect AND whenever zones/persons are added or renamed; when zone or person is deleted, publish empty retained payload to remove HA entity. Also missing: rebaseline command subscription wiring. Currently mqtt/client.go publishes discovery on connect but has no mechanism to detect zone/person CRUD events and re-publish or un-publish discovery configs. Needs event bus subscription for zone/person changes.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-02T18:26:21.696828674Z","updated_at":"2026-05-02T18:26:21.696828674Z","source_repo":".","compaction_level":0}
{"id":"bf-m6f5g","title":"GET /api/baseline and POST /api/baseline/capture endpoints","description":"Plan's REST API spec defines GET /api/baseline (returns [{link_id, snapshot_time, confidence}] for all links) and POST /api/baseline/capture (optional ?links body, starts 60s quiet-room capture). The baselines SQLite table exists (from migrations.go) and the baseline system runs internally, but no HTTP endpoints expose read/capture to the dashboard. The fleet handler has /api/nodes/:mac/rebaseline and /api/nodes/rebaseline-all but no standalone baseline endpoints.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:32.710840129Z","updated_at":"2026-05-05T17:55:00.504558262Z","closed_at":"2026-05-05T17:55:00.504558262Z","close_reason":"Completed","source_repo":".","compaction_level":0}

View file

@ -0,0 +1,16 @@
{
"bead_id": "bf-2povs",
"agent": "claude-code-glm-4.7",
"provider": "zai",
"model": "glm-4.7",
"exit_code": 0,
"outcome": "success",
"duration_ms": 220239,
"input_tokens": null,
"output_tokens": null,
"cost_usd": null,
"captured_at": "2026-05-06T04:27:37.530587158Z",
"trace_format": "claude_json",
"pruned": false,
"template_version": null
}

View file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
{
"bead_id": "bf-5y8tm",
"agent": "claude-code-glm-4.7",
"provider": "zai",
"model": "glm-4.7",
"exit_code": 124,
"outcome": "timeout",
"duration_ms": 600009,
"input_tokens": null,
"output_tokens": null,
"cost_usd": null,
"captured_at": "2026-05-06T04:51:59.970072537Z",
"trace_format": "claude_json",
"pruned": false,
"template_version": null
}

View file

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
b34892edfcb82b5137fd8bac58e96490fa7c0995
c6fd401694fc4eafff5df86433434d9c4b57d7dc

View file

@ -2319,6 +2319,7 @@
/**
* Rebuild Fresnel zone ellipsoids for all active links.
* Called when the overlay is toggled on or when links change.
* Shows zones 1-5 for each link, with zone 1 as green wireframe.
*/
function rebuildFresnelDebugEllipsoids() {
if (!state.fresnelDebugVisible) return;
@ -2333,6 +2334,9 @@
// Get node positions from Viz3D
var nodeMeshes = (window.Viz3D && Viz3D.getNodeMeshes) ? Viz3D.getNodeMeshes() : new Map();
// Configuration: number of Fresnel zones to show (1-5)
var maxZones = state.fresnelMaxZones || 5;
// Create ellipsoids for each active link
state.links.forEach(function(link, linkID) {
var parts = linkID.split(':');
@ -2352,41 +2356,61 @@
// Get channel from link's last CSI frame (default to 6 for 2.4 GHz)
var channel = link.channel || 6;
// Get per-link health score from Viz3D
var healthScore = 0.5;
if (window.Viz3D && Viz3D.getLinkHealth) {
var healthData = Viz3D.getLinkHealth(linkID);
if (healthData) healthScore = healthData.score;
}
var color = getFresnelHealthColor(healthScore);
// Create multiple Fresnel zones (1-5) for this link
// Zone 1 is green (most sensitive), zones 2-5 are cyan-to-blue gradient
var ellipsoids = window.Fresnel.addFresnelEllipsoidMultiZone(tx, rx, channel, maxZones, {
zone1Color: 0x66bb6a, // Green for zone 1
wireframeOpacity: 0.5, // Reduced opacity for better visibility
fillOpacity: 0.08 // Low fill opacity
});
// Create Fresnel ellipsoid
var ellipsoid = window.Fresnel.addFresnelEllipsoid(tx, rx, channel, color);
if (ellipsoid) {
// Store link info in userData for interactions
ellipsoid.wireframe.userData.linkID = linkID;
ellipsoid.wireframe.userData.txMAC = txMAC;
ellipsoid.wireframe.userData.rxMAC = rxMAC;
ellipsoid.wireframe.userData.healthScore = healthScore;
ellipsoid.fill.userData.linkID = linkID;
ellipsoid.fill.userData.txMAC = txMAC;
ellipsoid.fill.userData.rxMAC = rxMAC;
ellipsoid.fill.userData.healthScore = healthScore;
if (ellipsoids && ellipsoids.length > 0) {
// Store link info in userData for interactions on each ellipsoid
ellipsoids.forEach(function(ellipsoid, index) {
var zoneNumber = index + 1;
if (ellipsoid.wireframe) {
ellipsoid.wireframe.userData.linkID = linkID;
ellipsoid.wireframe.userData.zoneNumber = zoneNumber;
ellipsoid.wireframe.userData.txMAC = txMAC;
ellipsoid.wireframe.userData.rxMAC = rxMAC;
}
if (ellipsoid.fill) {
ellipsoid.fill.userData.linkID = linkID;
ellipsoid.fill.userData.zoneNumber = zoneNumber;
ellipsoid.fill.userData.txMAC = txMAC;
ellipsoid.fill.userData.rxMAC = rxMAC;
}
});
state.fresnelEllipsoids.set(linkID, ellipsoid);
// Store array of ellipsoids for this link
state.fresnelEllipsoids.set(linkID, ellipsoids);
}
});
console.log('[Fresnel Debug] Created ' + state.fresnelEllipsoids.size + ' Fresnel ellipsoids');
// Count total ellipsoids created
var totalEllipsoids = 0;
state.fresnelEllipsoids.forEach(function(ellipsoids) {
totalEllipsoids += ellipsoids.length;
});
console.log('[Fresnel Debug] Created ' + totalEllipsoids + ' Fresnel ellipsoids for ' + state.fresnelEllipsoids.size + ' links');
}
/**
* Clear all Fresnel debug ellipsoids from the scene.
*/
function clearFresnelDebugEllipsoids() {
state.fresnelEllipsoids.forEach(function(ellipsoid) {
if (window.Fresnel) {
window.Fresnel.removeFresnelEllipsoid(ellipsoid);
state.fresnelEllipsoids.forEach(function(ellipsoids) {
if (Array.isArray(ellipsoids)) {
ellipsoids.forEach(function(ellipsoid) {
if (window.Fresnel) {
window.Fresnel.removeFresnelEllipsoid(ellipsoid);
}
});
} else if (ellipsoids) {
// Legacy: single ellipsoid per link
if (window.Fresnel) {
window.Fresnel.removeFresnelEllipsoid(ellipsoids);
}
}
});
state.fresnelEllipsoids.clear();
@ -2419,10 +2443,23 @@
state.fresnelRaycaster.setFromCamera(state.fresnelMouse, camera);
var intersects = [];
state.fresnelEllipsoids.forEach(function(ellipsoid) {
var result = state.fresnelRaycaster.intersectObject(ellipsoid.fill, true);
if (result.length > 0) {
intersects.push(result[0]);
state.fresnelEllipsoids.forEach(function(ellipsoids) {
// ellipsoids is an array of ellipsoid objects (zones 1-5 for this link)
if (Array.isArray(ellipsoids)) {
ellipsoids.forEach(function(ellipsoid) {
if (ellipsoid.fill) {
var result = state.fresnelRaycaster.intersectObject(ellipsoid.fill, true);
if (result.length > 0) {
intersects.push(result[0]);
}
}
});
} else if (ellipsoids && ellipsoids.fill) {
// Legacy: single ellipsoid
var result = state.fresnelRaycaster.intersectObject(ellipsoids.fill, true);
if (result.length > 0) {
intersects.push(result[0]);
}
}
});
@ -2522,10 +2559,11 @@
}
var link = state.links.get(linkID);
var ellipsoid = state.fresnelEllipsoids.get(linkID);
if (!link || !ellipsoid) return;
var ellipsoids = state.fresnelEllipsoids.get(linkID);
if (!link || !ellipsoids || !Array.isArray(ellipsoids) || ellipsoids.length === 0) return;
var data = ellipsoid.data;
// Get data from the first ellipsoid (zone 1)
var data = ellipsoids[0].data;
var healthScore = 0.5;
if (window.Viz3D && Viz3D.getLinkHealth) {
var healthData = Viz3D.getLinkHealth(linkID);
@ -2539,11 +2577,15 @@
var txLabel = txNode ? (txNode.name || txNode.mac) : txMAC;
var rxLabel = rxNode ? (rxNode.name || rxNode.mac) : rxMAC;
// Count how many zones are shown
var zoneCount = ellipsoids.length;
tooltip.innerHTML =
'<strong>Link:</strong> ' + txLabel + ' to ' + rxLabel + '<br>' +
'<strong>Fresnel radius at midpoint:</strong> ' + data.b.toFixed(2) + ' m<br>' +
'<strong>Fresnel zones:</strong> ' + zoneCount + ' (zone 1 shown in green)<br>' +
'<strong>Zone 1 radius at midpoint:</strong> ' + data.b.toFixed(2) + ' m<br>' +
'<strong>Link distance:</strong> ' + data.d.toFixed(2) + ' m<br>' +
'<strong>Wavelength:</strong> ' + data.lambda.toFixed(3) + ' m (ch ' + data.channel + ')<br>' +
'<strong>Wavelength:</strong> ' + (data.lambda * 1000).toFixed(1) + ' mm (ch ' + data.channel + ')<br>' +
'<strong>Link health:</strong> ' + Math.round(healthScore * 100) + '%';
tooltip.style.display = 'block';

View file

@ -77,15 +77,34 @@
* @returns {Object} Ellipsoid parameters: { center, semiAxes, rotation, lambda, d, a, b }
*/
function calculateFresnelEllipsoid(tx, rx, channel) {
return calculateFresnelEllipsoidForZone(tx, rx, channel, 1);
}
/**
* Calculate Fresnel zone ellipsoid parameters for a specific zone number.
* Based on the nth Fresnel zone geometry.
*
* For the nth Fresnel zone:
* - Semi-major axis: a = (d + n*lambda/2) / 2
* - Semi-minor axis: b = sqrt(a^2 - (d/2)^2)
*
* @param {THREE.Vector3} tx - Transmitter position
* @param {THREE.Vector3} rx - Receiver position
* @param {number} channel - WiFi channel number (for wavelength)
* @param {number} zoneNumber - Fresnel zone number (1-based, typically 1-5)
* @returns {Object} Ellipsoid parameters: { center, semiAxes, rotation, lambda, d, a, b, zoneNumber }
*/
function calculateFresnelEllipsoidForZone(tx, rx, channel, zoneNumber) {
// Get wavelength based on channel
const lambda = getWavelengthForChannel(channel);
// Direct distance between TX and RX
const d = tx.distanceTo(rx);
// First Fresnel zone ellipsoid parameters
// Semi-major axis: a = (d + lambda/2) / 2
const a = (d + lambda / 2) / 2;
// nth Fresnel zone ellipsoid parameters
// Semi-major axis: a = (d + n*lambda/2) / 2
const n = Math.max(1, zoneNumber);
const a = (d + n * lambda / 2) / 2;
// Semi-minor axis: b = sqrt(a^2 - (d/2)^2)
// Using the property that for a prolate spheroid with foci at tx and rx:
@ -109,6 +128,7 @@
d: d,
a: a,
b: b,
zoneNumber: n,
channel: channel
};
}
@ -121,7 +141,7 @@
* @param {THREE.Vector3} rx - Receiver position
* @param {number} channel - WiFi channel number
* @param {number} color - Color hex value (e.g., 0x4FC3F7 for blue)
* @param {Object} options - Optional settings { wireframeOpacity, fillOpacity }
* @param {Object} options - Optional settings { wireframeOpacity, fillOpacity, zoneNumber }
* @returns {Object} Object containing { wireframe, fill, data } meshes
*/
function FresnelEllipsoid(tx, rx, channel, color, options) {
@ -133,9 +153,10 @@
options = options || {};
const wireframeOpacity = options.wireframeOpacity !== undefined ? options.wireframeOpacity : CONFIG.wireframeOpacity;
const fillOpacity = options.fillOpacity !== undefined ? options.fillOpacity : CONFIG.fillOpacity;
const zoneNumber = options.zoneNumber || 1;
// Calculate ellipsoid geometry
const ellipsoid = calculateFresnelEllipsoid(tx, rx, channel);
// Calculate ellipsoid geometry for the specified zone
const ellipsoid = calculateFresnelEllipsoidForZone(tx, rx, channel, zoneNumber);
// Determine segment count based on viewport (mobile optimization)
const isMobile = window.innerWidth < CONFIG.mobileViewportWidth;
@ -183,6 +204,7 @@
tx: tx.clone(),
rx: rx.clone(),
channel: channel,
zoneNumber: ellipsoid.zoneNumber,
lambda: ellipsoid.lambda,
d: ellipsoid.d,
a: ellipsoid.a,
@ -246,14 +268,70 @@
}
}
/**
* Add multiple Fresnel zone ellipsoids (zones 1-maxZone) for a single link.
* Creates wireframe-only ellipsoids for zones 2-5 to avoid visual clutter.
*
* @param {THREE.Vector3} tx - Transmitter position
* @param {THREE.Vector3} rx - Receiver position
* @param {number} channel - WiFi channel number
* @param {number} maxZone - Maximum zone number to create (default 5)
* @param {Object} options - Optional settings { zone1Color, zoneColors, wireframeOpacity }
* @returns {Array} Array of ellipsoid objects, one per zone
*/
function addFresnelEllipsoidMultiZone(tx, rx, channel, maxZone, options) {
if (!_scene) {
console.warn('[Fresnel] Scene not initialized. Call Fresnel.init(scene) first.');
return [];
}
maxZone = maxZone || 5;
options = options || {};
// Zone 1 is green (most sensitive)
const zone1Color = options.zone1Color || 0x66bb6a;
// Colors for zones 2-5 (gradient from cyan to blue)
const zoneColors = options.zoneColors || [0x4dd0e1, 0x26c6da, 0x00bcd4, 0x0097a7];
const wireframeOpacity = options.wireframeOpacity !== undefined ? options.wireframeOpacity : CONFIG.wireframeOpacity;
const fillOpacity = options.fillOpacity !== undefined ? options.fillOpacity : CONFIG.fillOpacity;
const ellipsoids = [];
for (let n = 1; n <= maxZone; n++) {
const color = (n === 1) ? zone1Color : (zoneColors[n - 2] || 0x0097a7);
// For zones 2+, use wireframe only to reduce visual clutter
const zoneOptions = {
wireframeOpacity: wireframeOpacity,
fillOpacity: (n === 1) ? fillOpacity : 0, // No fill for zones 2+
zoneNumber: n
};
const ellipsoid = FresnelEllipsoid(tx, rx, channel, color, zoneOptions);
if (ellipsoid) {
if (_scene) {
_scene.add(ellipsoid.wireframe);
if (ellipsoid.fill) _scene.add(ellipsoid.fill);
}
ellipsoids.push(ellipsoid);
}
}
return ellipsoids;
}
// ============================================
// Public API
// ============================================
window.Fresnel = {
init: init,
calculateFresnelEllipsoid: calculateFresnelEllipsoid,
calculateFresnelEllipsoidForZone: calculateFresnelEllipsoidForZone,
FresnelEllipsoid: FresnelEllipsoid,
addFresnelEllipsoid: addFresnelEllipsoid,
addFresnelEllipsoidMultiZone: addFresnelEllipsoidMultiZone,
removeFresnelEllipsoid: removeFresnelEllipsoid,
// Configuration access
CONFIG: CONFIG

BIN
sim Executable file

Binary file not shown.