diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5164b79..8f96520 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -46,7 +46,7 @@ {"id":"spaxel-3ps","title":"Detection feedback loop and accuracy tracking","description":"## Background\n\nEvery detection algorithm produces errors. False positives (detected presence when no one is there) are annoying and erode trust. False negatives (missed detection of a real person) are dangerous for safety applications. The feedback loop gives users a direct mechanism to correct errors and the system learns from those corrections. Showing users measurable improvement over time (\"You've provided 47 corrections. Accuracy improved 12% this week\") creates a virtuous engagement loop and transforms users into active participants in improving the system.\n\n## Feedback UI Elements\n\nEvery detection event exposed to the user should have feedback affordances. Three contexts:\n\n1. Dashboard 3D view: Each active track has a small thumbs-up/down icon that appears on hover/focus. Clicking thumbs-down opens a quick inline form.\n\n2. Activity timeline (Phase 8): Every detection event entry has thumbs-up/thumbs-down at the end of the row. Space-efficient: 2 icon buttons.\n\n3. Push notifications: Fall and anomaly notifications include a quick-reply option (via ntfy actions or Pushover callbacks): \"False alarm — clear this.\"\n\n4. \"I was here and wasn't detected\" button: On the timeline panel, a button \"Report missed detection\" opens a form: \"When? [time picker, default: now]\", \"Where? [zone picker]\", \"Who? [person picker, optional]\". Submits as a FALSE_NEGATIVE feedback event with the user-provided position.\n\nFeedback form for thumbs-down:\n- \"What was wrong?\" (radio buttons):\n - \"No one was there (false alarm)\"\n - \"Someone was missed at this location\"\n - \"Wrong person identified\"\n - \"Wrong zone/location\"\n- Optional free-text \"Notes\" field\n- Submit / Cancel\n\n## Feedback Storage\n\nSQLite schema:\nCREATE TABLE detection_feedback (\n id TEXT PRIMARY KEY,\n event_id TEXT, -- references events table (activity timeline)\n event_type TEXT, -- \"blob_detection\", \"zone_transition\", \"fall_alert\", \"anomaly\"\n feedback_type TEXT, -- \"TRUE_POSITIVE\", \"FALSE_POSITIVE\", \"FALSE_NEGATIVE\", \"WRONG_IDENTITY\", \"WRONG_ZONE\"\n details_json TEXT, -- {\"zone_id\":\"...\", \"person_id\":\"...\", \"notes\":\"...\"}\n timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,\n applied BOOLEAN DEFAULT FALSE, -- set to TRUE after weight refinement processes it\n processed_at DATETIME\n);\n\nThe applied flag enables incremental processing: the weight learner (Phase 7 self-improving localisation) queries WHERE applied = FALSE, processes batches, and marks them TRUE.\n\n## Accuracy Metrics\n\nCompute precision/recall/F1 per link, per zone, and per person weekly. This requires knowing the true positives, false positives, and false negatives.\n\nGround truth sources:\n- User thumbs-up -> TRUE_POSITIVE for the corresponding detection event\n- User thumbs-down (false alarm) -> FALSE_POSITIVE for the detection event\n- User \"missed detection\" report -> FALSE_NEGATIVE for the reported time/zone\n\nNote: ground truth is sparse — users will not feedback every event. We use the feedback we have as a sample. Assume events without feedback are TRUE_POSITIVE for the purpose of precision estimates (conservative: this means precision is an upper bound, not exact).\n\nMetrics computed weekly:\n- precision = TP / (TP + FP) — of all detections, what fraction were correct\n- recall = TP / (TP + FN) — of all true presence events, what fraction were detected\n- F1 = 2 * precision * recall / (precision + recall)\n- Per-link metrics: which links have the most false positives (worst precision)\n- Per-zone metrics: which zones are most often missed (worst recall)\n\nStorage: detection_accuracy (week TEXT, scope_type TEXT, scope_id TEXT, precision REAL, recall REAL, f1 REAL, tp_count INT, fp_count INT, fn_count INT, computed_at DATETIME). Scope types: \"system\", \"link\", \"zone\", \"person\".\n\n## Accuracy Trend Display\n\nDashboard \"Accuracy\" panel (in expert mode):\n- Overall accuracy gauge: composite F1 score as a circular gauge (0-100%)\n- Week-over-week trend graph: sparkline of weekly F1 over the last 8 weeks\n- \"You've provided N corrections. Your accuracy improved X% this week.\" — motivational counter\n- Per-zone breakdown: bar chart of precision/recall per zone (click a zone bar to jump to it in 3D view)\n- Per-link breakdown: link health vs. feedback score correlation (are high-health links also high-accuracy?)\n- Feedback count: total corrections given, open corrections (not yet processed), processed corrections\n\nThe accuracy trend display intentionally shows the improvement trajectory, not just the absolute value, to reinforce that feedback has an effect.\n\n## Feedback Application\n\nProcessing happens in a background goroutine (mothership/internal/learning/feedback_processor.go) that runs every 6 hours or when triggered manually.\n\nFor FALSE_POSITIVE events with associated CSI data (in the recording buffer from Phase 2):\n- Retrieve the CSI data from the recording buffer at the event timestamp for all links\n- Add the CSI frame data to a \"known false positive\" set in SQLite: false_positive_frames (link_id, timestamp, delta_rms, context_json)\n- The weight learner (self-improving localisation bead) uses this set as negative examples\n\nFor FALSE_NEGATIVE events with user-reported position:\n- Add to \"known false negative\" set: false_negative_frames (link_id, timestamp, expected_position_xyz, context_json)\n- The weight learner uses this as a positive example at the specified position\n\nAfter processing, mark feedback.applied = TRUE.\n\n## Files to Create or Modify\n\n- mothership/internal/learning/feedback_processor.go: feedback processing pipeline\n- mothership/internal/analytics/accuracy.go: weekly metric computation\n- dashboard/js/feedback.js: thumbs-up/down UI components (reusable across 3D view and timeline)\n- dashboard/js/accuracy.js: Accuracy panel rendering\n- mothership/internal/dashboard/routes.go: POST /api/feedback, GET /api/accuracy\n\n## Tests\n\n- Test feedback storage: POST /api/feedback with each feedback_type, verify SQLite record created\n- Test accuracy metric computation with synthetic TP/FP/FN data: 8 TP, 2 FP, 1 FN -> precision=0.8, recall=0.888\n- Test weekly rollup: 7 days of daily feedback -> correctly aggregated weekly metric\n- Test that applied=false events are found and marked as applied after processor run\n- Test \"improvements\" counter: feedback_count increases on each POST /api/feedback call\n\n## Acceptance Criteria\n\n- Thumbs-up/down buttons appear on active tracks in 3D view and on all timeline events\n- \"Missed detection\" button and form available in timeline panel\n- Feedback stored in SQLite with correct feedback_type and details\n- Accuracy metrics computed weekly and stored in detection_accuracy table\n- Accuracy panel shows week-over-week trend (requires at least 2 weeks of data)\n- Feedback improvement counter shows correct counts\n- Applied flag correctly set after processor run\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"sp4","owner":"","created_at":"2026-03-28T01:49:50.419277632Z","created_by":"coding","updated_at":"2026-03-29T22:08:03.778130122Z","closed_at":"2026-03-29T22:08:03.778000167Z","close_reason":"Implementation complete: feedback storage (SQLite), accuracy computation (precision/recall/F1 weekly), feedback processor (6h interval), API endpoints (/api/learning/*), frontend feedback UI (thumbs up/down, missed detection form), accuracy panel (F1 gauge, sparkline, per-zone breakdown). All 12 tests pass.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"],"dependencies":[{"issue_id":"spaxel-3ps","depends_on_id":"spaxel-zvs","type":"blocks","created_at":"2026-03-28T03:29:14.442377218Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-3rd","title":"Wire WebSocket integration for zone changes","description":"Ensure zone changes from CRUD endpoints reflect in live 3D view within one WebSocket cycle. Acceptance: creating/updating/deleting a zone via REST API triggers an update broadcast through the WebSocket system.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-07T17:01:33.587080369Z","created_by":"coding","updated_at":"2026-04-07T18:42:55.455708044Z","closed_at":"2026-04-07T18:42:55.455446177Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","mitosis-child","mitosis-depth:1","parent-spaxel-0ii"]} {"id":"spaxel-403","title":"Implement anomaly detection & security mode","description":"Build pattern learning and anomaly detection for security.\n\nDeliverables:\n- 7-day pattern learning algorithm\n- Anomaly scoring against learned patterns\n- Security mode integration\n\nAcceptance: System detects deviations from learned patterns; accuracy improves measurably over 4 weeks.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-03-29T19:25:04.187535979Z","created_by":"coding","updated_at":"2026-04-09T12:18:14.752621360Z","closed_at":"2026-04-09T12:18:14.752279788Z","close_reason":"Anomaly detection & security mode implementation verified complete.\n\nDeliverables implemented:\n- 7-day pattern learning algorithm with Welford's online algorithm (analytics/patterns.go)\n- Anomaly scoring against learned patterns with z-score based computation\n- Security mode integration with Armed/Disarmed/ArmedStay states\n\nAcceptance criteria met:\n- System detects deviations from learned patterns via multiple anomaly types (UnusualHour, UnknownBLE, MotionDuringAway, UnusualDwell)\n- Accuracy improves measurably through feedback loop integration with learning/feedback_store\n\nKey components:\n- PatternLearner: 7-day cold start, hourly pattern updates, per-slot readiness checking\n- Detector: Multiple anomaly types, configurable thresholds, alert chain with timers\n- Security API: /api/security/arm, /api/security/disarm, /api/security/status\n- Alert Handler: Dashboard → webhook → escalation notification chain\n- Integration: Fully wired in main.go with zones, BLE registry, dashboard, and feedback store","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:922","mitosis-child","mitosis-depth:1","parent-spaxel-i28"]} -{"id":"spaxel-40tl","title":"Write comprehensive tests for notification system","description":"Add test files for all notification components. Tests must cover: floor-plan renderer produces 300x300 PNG with correct dimensions, zone boundaries appear at correct pixel coordinates, batching behavior (3 LOW events in 10s -> 1 notification, 1 URGENT -> immediate), quiet hours gate (LOW at 23:00 with 22:00-07:00 quiet hours -> queued, URGENT at 23:00 -> delivered), morning digest delivery bundles queued events at quiet_hours_end, ntfy delivery with mock HTTP server verifies headers/body, webhook delivery verifies JSON structure and base64 PNG field, test-notification endpoint fires correctly.\n\nAcceptance Criteria:\n- All renderer tests pass (dimensions, coordinates, colors)\n- All batching tests pass (windowing, priority bypass)\n- All quiet hours tests pass (queueing, bypass, digest)\n- All delivery client tests pass with mocks\n- Test endpoint integration test passes\n- Test coverage >= 80% for notification packages","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-10T12:19:08.646045806Z","created_by":"coding","updated_at":"2026-05-04T06:47:04.344233758Z","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:8","mitosis-child","mitosis-depth:1","parent-spaxel-zpt"],"dependencies":[{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-0fm8","type":"blocks","created_at":"2026-04-11T08:15:08.008025729Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-16z3","type":"blocks","created_at":"2026-04-11T08:15:08.148112051Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-28j7","type":"blocks","created_at":"2026-04-11T08:15:07.907058347Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-35lb","type":"blocks","created_at":"2026-04-11T08:15:08.208324942Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-4frg","type":"blocks","created_at":"2026-04-11T08:15:08.056467899Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-k0rs","type":"blocks","created_at":"2026-04-11T08:15:07.972516975Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-wekq","type":"blocks","created_at":"2026-04-11T08:15:08.101758877Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-40tl","title":"Write comprehensive tests for notification system","description":"Add test files for all notification components. Tests must cover: floor-plan renderer produces 300x300 PNG with correct dimensions, zone boundaries appear at correct pixel coordinates, batching behavior (3 LOW events in 10s -> 1 notification, 1 URGENT -> immediate), quiet hours gate (LOW at 23:00 with 22:00-07:00 quiet hours -> queued, URGENT at 23:00 -> delivered), morning digest delivery bundles queued events at quiet_hours_end, ntfy delivery with mock HTTP server verifies headers/body, webhook delivery verifies JSON structure and base64 PNG field, test-notification endpoint fires correctly.\n\nAcceptance Criteria:\n- All renderer tests pass (dimensions, coordinates, colors)\n- All batching tests pass (windowing, priority bypass)\n- All quiet hours tests pass (queueing, bypass, digest)\n- All delivery client tests pass with mocks\n- Test endpoint integration test passes\n- Test coverage >= 80% for notification packages","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-10T12:19:08.646045806Z","created_by":"coding","updated_at":"2026-05-04T07:01:25.142023082Z","closed_at":"2026-05-04T07:01:25.142023082Z","close_reason":"Added comprehensive tests for notification system - Floor plan renderer tests - Batching behavior tests - Quiet hours tests - Delivery client tests - Test endpoint integration - Test coverage: 87.8%","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:8","mitosis-child","mitosis-depth:1","parent-spaxel-zpt"],"dependencies":[{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-0fm8","type":"blocks","created_at":"2026-04-11T08:15:08.008025729Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-16z3","type":"blocks","created_at":"2026-04-11T08:15:08.148112051Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-28j7","type":"blocks","created_at":"2026-04-11T08:15:07.907058347Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-35lb","type":"blocks","created_at":"2026-04-11T08:15:08.208324942Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-4frg","type":"blocks","created_at":"2026-04-11T08:15:08.056467899Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-k0rs","type":"blocks","created_at":"2026-04-11T08:15:07.972516975Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-40tl","depends_on_id":"spaxel-wekq","type":"blocks","created_at":"2026-04-11T08:15:08.101758877Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-4fg","title":"Implement Replay/Time-Travel REST endpoints","description":"Implement GET /api/replay/sessions to list recording sessions. Add POST endpoints: /api/replay/start to start replay at timestamp, /api/replay/stop to return to live, /api/replay/seek to seek within session, /api/replay/tune to update pipeline parameters mid-replay. Include OpenAPI-style godoc comments.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T15:31:10.497876498Z","created_by":"coding","updated_at":"2026-04-07T13:20:09.903154198Z","closed_at":"2026-04-07T13:20:09.902983511Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["mitosis-child","mitosis-depth:1","parent-spaxel-6ha"]} {"id":"spaxel-4frg","title":"Add morning digest tests","description":"Write tests for morning digest delivery: bundles queued events at quiet_hours_end. Acceptance Criteria: Morning digest tests pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-11T08:15:08.033947792Z","created_by":"coding","updated_at":"2026-04-11T09:24:34.821263381Z","closed_at":"2026-04-11T09:24:34.821202594Z","close_reason":"Morning digest tests implemented and passing. All 8 morning digest tests pass covering queuing at quiet_hours_end, disabled behavior, once-per-day, empty handling, event inclusion, queue clearing, mixed priorities, and title format. Acceptance criteria met.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:3","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} {"id":"spaxel-4u6","title":"events: SQLite schema, FTS5 table, indexes, and 90-day archive job","description":"## Overview\nCreate the SQLite storage layer for the unified activity timeline (part 1 of spaxel-2ap split).\n\n## Schema to create in mothership/internal/events/ (db setup or migration)\n```sql\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n zone TEXT,\n person TEXT,\n blob_id TEXT,\n detail_json TEXT,\n severity TEXT\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_events USING fts5(\n type, zone, person, detail_json,\n content='events', content_rowid='id'\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp_ms DESC);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(type);\nCREATE INDEX IF NOT EXISTS idx_events_zone ON events(zone);\nCREATE INDEX IF NOT EXISTS idx_events_person ON events(person);\n\nCREATE TABLE IF NOT EXISTS events_archive (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n zone TEXT, person TEXT, blob_id TEXT, detail_json TEXT, severity TEXT\n);\n```\n\n## Archive job\n- In `events` package, add `RunArchiveJob(db *sql.DB)` that runs nightly at 02:00 local time\n- Migrates rows from `events` where `timestamp_ms < now - 90 days` into `events_archive`\n- Deletes moved rows from `events`\n\n## Go types\n```go\ntype Event struct {\n ID int64\n Type string\n TimestampMs int64\n Zone string\n Person string\n BlobID string\n DetailJSON string\n Severity string\n}\n\nfunc InsertEvent(db *sql.DB, e Event) error\nfunc QueryEvents(db *sql.DB, params QueryParams) ([]Event, string, bool, error)\n```\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/events/\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-06T22:30:57.090344045Z","created_by":"coding","updated_at":"2026-04-07T16:45:36.428356135Z","closed_at":"2026-04-07T16:45:36.428249897Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:2"]} @@ -196,7 +196,7 @@ {"id":"spaxel-sty","title":"CSI simulator Go CLI","description":"## Background\n\nThe pre-deployment simulator (spaxel-btj) provides a browser-based spatial planning tool. The CSI simulator CLI is a complementary developer tool: a standalone Go command-line program that opens WebSocket connections to a running mothership as virtual nodes and injects synthetic CSI binary frames. This enables automated integration testing, performance benchmarking, and algorithm development entirely without ESP32 hardware.\n\nThe critical difference: the pre-deployment simulator generates CSI on the server side (using the simulation API). The CLI simulator generates CSI externally and connects via the standard node WebSocket interface — testing the full network stack and protocol, not just the signal processing.\n\n## CLI Implementation\n\nNew Go command: mothership/cmd/sim/main.go\n\nUsage examples:\n sim --mothership localhost:8080 --nodes 4 --walkers 2 --rate 20 --duration 60\n sim --mothership localhost:8080 --token abc123def456 --nodes 2 --walkers 1 --seed 42\n sim --space \"6x4x2.5\" --nodes 4 --walkers 3 --rate 50 --duration 120 --ble\n sim --verify --mothership localhost:8080 --nodes 2 --walkers 1 --duration 10\n\nCLI Flags:\n- --mothership: URL of the mothership (default: ws://localhost:8080/ws)\n- --token: provision token. If not specified, automatically provisions via POST /api/provision with synthetic credentials.\n- --nodes: number of virtual nodes (default 2). Each node opens a separate WebSocket connection.\n- --walkers: number of synthetic walkers (default 1).\n- --rate: CSI transmission rate in Hz per node pair (default 20).\n- --duration: total run time in seconds (default 60). \"0\" means run until Ctrl+C.\n- --seed: random seed for reproducible walker paths (default: random seed, logged at startup).\n- --space: room dimensions in \"WxDxH\" format (default \"5x5x2.5\").\n- --ble: include synthetic BLE advertisements (one BLE device per walker, with stable random MAC).\n- --verify: after --duration seconds, verify that the mothership produced the expected number of blobs. Exit 0 on success, 1 on failure.\n- --noise-sigma: Gaussian noise standard deviation for I/Q generation (default 0.005).\n- --wall: add a wall as \"x1,y1,x2,y2\" (can be repeated). Walls affect the path loss model.\n- --output-csv: write synthetic ground truth (walker positions + link deltaRMS) to a CSV file for offline analysis.\n\n## Synthetic CSI Frame Generation\n\nFor each virtual node pair (TX, RX) and each walker at each timestep:\n\n1. Compute RSSI from path loss model (same as simulator physics, mothership/internal/simulator/physics.go — reuse this package).\n\n2. Compute deltaRMS from Fresnel zone overlap (same physics model).\n\n3. Generate binary CSI frame matching the Phase 1 protocol format:\n Header (24 bytes):\n - Magic: 0xABCDEF01 (4 bytes)\n - Version: 1 (1 byte)\n - Node MAC: 6 bytes (synthetic, consistent per virtual node)\n - Peer MAC: 6 bytes (TX node's MAC for RX-side frames)\n - Channel: 6 (2.4GHz ch 6) or 36 (5GHz) — configurable\n - RSSI: 1 byte (signed, from path loss calculation)\n - Num subcarriers: 64 (1 byte)\n - Timestamp_us: 8 bytes (current Unix microseconds)\n\n Payload (128 bytes = 64 subcarriers * 2 bytes each):\n - For each subcarrier i: I = amplitude * cos(phase_i) + noise, Q = amplitude * sin(phase_i) + noise\n - amplitude: from Fresnel zone deltaRMS model\n - phase_i: phase_base + i * phase_step + phase_noise (simulate subcarrier phase variation)\n - noise: gaussian(sigma=--noise-sigma)\n - Values are int8 (clamped to [-127, 127])\n\nThe frame format is validated against the actual firmware output by comparing to real recorded frames in docs/research/reference_frames.bin (if available).\n\n## Connection Protocol\n\nEach virtual node:\n1. Opens WebSocket to ws://{mothership}/ws\n2. Sends hello message: {\"type\":\"hello\",\"mac\":\"{virtual_mac}\",\"firmware_version\":\"sim-1.0.0\",\"capabilities\":{\"can_tx\":true,\"can_rx\":true},\"free_heap\":200000,\"wifi_rssi\":-45,\"ip_addr\":\"127.0.0.{n}\"}\n3. Waits for role push from mothership (expects {\"type\":\"role\",\"role\":N})\n4. If receives {\"type\":\"reject\"}: logs error and exits with code 2\n5. Begins sending CSI frames at the configured rate using the binary WebSocket message format (not JSON)\n6. Also sends health messages every 10s: {\"type\":\"health\",\"heap\":200000,\"rssi\":-45,\"uptime_s\":N}\n7. If --ble: sends BLE relay messages every 5s: {\"type\":\"ble\",\"devices\":[{\"mac\":\"...\",\"name\":\"sim-person-1\",\"rssi\":-60}]}\n\n## Verification Mode (--verify)\n\nAfter --duration seconds:\n1. Stop sending CSI frames\n2. Wait 2 seconds for pipeline to settle\n3. GET {mothership}/api/blobs — gets current list of active blobs\n4. Assert: blob_count == walker_count (within ±1 tolerance)\n5. If all walkers are within the room bounds: assert all walkers have a blob within 2m distance\n6. Print: \"PASS: {blob_count} blobs detected for {walker_count} walkers\" or \"FAIL: expected N blobs, got M\"\n7. Exit 0 (PASS) or 1 (FAIL)\n\nThis is the CI smoke test that verifies the full pipeline end-to-end without hardware.\n\n## CI Integration\n\nAdd a CI step to the mothership GitHub Actions workflow (if one exists, or document the command):\n1. Start mothership with test config (in-memory SQLite, no recording)\n2. Run: sim --verify --nodes 2 --walkers 1 --duration 10 --seed 42\n3. Assert exit code 0\n\nThis becomes the primary end-to-end integration test. If the sim fails to produce blobs, something in the pipeline is broken.\n\n## Performance Testing\n\nThe simulator supports high-throughput testing:\n- sim --nodes 16 --walkers 4 --rate 50 --duration 60: measures mothership throughput at 16 nodes * 50 Hz = 800 frames/second\n- The simulator prints throughput statistics at the end: frames sent, frames per second, CPU time\n- Use for benchmarking and profiling the mothership processing pipeline\n\n## Files to Create\n\n- mothership/cmd/sim/main.go: CLI entry point with all flags\n- mothership/cmd/sim/generator.go: synthetic CSI frame generator\n- mothership/cmd/sim/walker.go: synthetic walker movement simulation\n- mothership/cmd/sim/verify.go: blob count verification logic\n- mothership/internal/simulator/physics.go: reuse from pre-deployment simulator (shared package)\n\n## Tests\n\n- Test that generated frames have the correct binary header format (magic, version bytes in correct positions)\n- Test that RSSI value is within plausible range for the given walker distance (e.g. walker at 2m, wall_attenuation=0 -> RSSI in [-50, -70])\n- Test that generated I/Q values are clamped to int8 range [-127, 127]\n- Test hello message format matches what the mothership ingestion server expects (parsed successfully)\n- Test that --verify correctly detects missing blobs (inject 1 walker, mock mothership returns 0 blobs -> FAIL)\n- Test --seed 42 produces identical walker paths across two runs (reproducibility)\n- Test --output-csv generates a CSV with correct headers and ground truth positions\n\n## Acceptance Criteria\n\n- CLI connects and streams synthetic CSI to a running mothership\n- Mothership blob count equals walker count (within ±1) when --verify is used\n- CLI exits cleanly after --duration seconds\n- --verify returns exit code 0 when blobs match, exit code 1 when they don't\n- Works correctly in a CI environment without hardware\n- High-throughput test (16 nodes, 50 Hz) completes without mothership errors or OOM\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-03-28T01:57:48.145516684Z","created_by":"coding","updated_at":"2026-04-25T01:53:18.591116700Z","closed_at":"2026-04-25T01:53:18.591003707Z","close_reason":"CSI simulator CLI already fully implemented and tested in mothership/cmd/sim/. All 13 tests pass, go vet clean. Features include: all CLI flags, synthetic CSI binary frame generation, Fresnel zone physics, walker simulation, BLE, blob verification, CSV output.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"],"dependencies":[{"issue_id":"spaxel-sty","depends_on_id":"spaxel-i28","type":"blocks","created_at":"2026-03-28T03:29:14.669138899Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-tg13","title":"Replace Simple/Expert mode toggle with progressive disclosure","description":"## Goal\n\nPer plan.md §8e \"Information architecture\", the Simple vs Expert toggle is removed. The home page is the simple surface by default; advanced views live at their own routes (`/live`, `/setup`).\n\n## Scope\n\n1. Remove the Simple/Expert toggle button from all dashboard pages.\n2. Remove the `localStorage` key that tracks the user's mode preference.\n3. Ensure `/live` is reachable from the home page (a link on the Devices & Fleet Health card is acceptable; also from the nav).\n4. Delete `simple.html` if its content is fully subsumed by the new home page, or repurpose it as the new home and delete `index.html` if cleaner.\n\n## Verification\n\n- No \"Simple\" or \"Expert\" UI text anywhere on the dashboard.\n- `grep -rE \"simpleMode|expertMode|simple.?expert\" dashboard/` returns no results outside of this bead's cleanup commits.\n- Users can reach the 3D view via a visible nav link, not a mode toggle.\n\nDepends on spaxel-rniz (home page restructure) landing first.\n\nTraceability: plan.md §8e \"Information architecture\", §8f item 8.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"feature","assignee":"quebec","owner":"","created_at":"2026-04-24T11:14:08.015422752Z","created_by":"coding","updated_at":"2026-04-25T03:10:36.990669644Z","closed_at":"2026-04-25T03:10:36.990606159Z","close_reason":"No-op: Simple/Expert mode toggle was never implemented. Prerequisite spaxel-rniz already established progressive disclosure with separate routes (/ home, /live 3D, /fleet, /setup). All verification criteria pass: no Simple/Expert text, no simpleMode/expertMode keys, /live reachable from home via nav and card links.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:8"],"dependencies":[{"issue_id":"spaxel-tg13","depends_on_id":"spaxel-rniz","type":"blocks","created_at":"2026-04-24T11:16:07.378306962Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-tgj","title":"Home automation integration (MQTT and webhooks)","description":"## Background\n\nMany home automation systems — Home Assistant, OpenHAB, Node-RED, Domoticz — use MQTT as their primary integration backbone. By publishing spaxel state as MQTT topics with Home Assistant auto-discovery payloads, spaxel becomes a first-class presence sensor in any HA setup. No custom integration code is needed: the entities appear automatically in Home Assistant's entity registry simply by being powered on and having MQTT configured. This makes spaxel accessible to millions of home automation users who already have HA running.\n\n## MQTT Client\n\nOptional MQTT client in mothership/internal/mqtt/client.go.\n\nThe client is optional — if MQTT is not configured, the module is a no-op. Configuration via dashboard Settings or mothership config file:\n- broker_url: e.g. \"tcp://homeassistant.local:1883\" or \"mqtt://192.168.1.100:1883\"\n- username, password (optional)\n- tls: true/false\n- client_id: \"spaxel-{mothership_id}\" (unique per mothership instance)\n- retain: true (retained messages for presence and occupancy topics)\n\nUse the github.com/eclipse/paho.mqtt.golang library (the de-facto standard Go MQTT client).\n\nReconnect policy: exponential backoff from 5s to 120s, indefinite retry. Log reconnect attempts.\n\n## MQTT Topic Structure\n\nAll topics are prefixed with \"spaxel/{mothership_id}/\" where mothership_id is the unique ID from the mothership config.\n\nPerson presence:\n- Topic: spaxel/{mothership_id}/person/{person_id}/presence\n- Payload: \"home\" or \"not_home\" (plain string, retained)\n- Updated when: a person's first labelled BLE device is seen (home) or all their BLE devices have been absent for > 15 minutes (not_home)\n\nZone occupancy:\n- Topic: spaxel/{mothership_id}/zone/{zone_id}/occupancy\n- Payload: integer count (plain string, retained, e.g. \"2\")\n- Topic: spaxel/{mothership_id}/zone/{zone_id}/occupants\n- Payload: JSON array of person names (retained, e.g. [\"Alice\",\"Bob\"])\n- Updated on every zone occupancy change\n\nFall detection:\n- Topic: spaxel/{mothership_id}/fall_detected\n- Payload: JSON {\"person_id\":\"...\",\"person_label\":\"Alice\",\"zone_id\":\"...\",\"zone_name\":\"Hallway\",\"timestamp\":\"2026-03-27T14:23:00Z\"}\n- Not retained (event topic)\n\nSystem health:\n- Topic: spaxel/{mothership_id}/system/health\n- Payload: JSON {\"node_count\":4,\"online_count\":4,\"detection_quality\":0.87,\"mode\":\"home\"}\n- Retained, updated every 60s\n\n## Home Assistant Auto-Discovery\n\nOn initial MQTT connection (and on reconnect), publish HA auto-discovery payloads to the homeassistant/ prefix topics. HA processes these automatically and adds the entities to its registry.\n\nPer person: binary_sensor (presence)\nDiscovery topic: homeassistant/binary_sensor/spaxel_{mothership_id}_{person_id}/config\nPayload (JSON, retained):\n{\n \"name\": \"Alice Presence\",\n \"unique_id\": \"spaxel_{mothership_id}_{person_id}_presence\",\n \"state_topic\": \"spaxel/{mothership_id}/person/{person_id}/presence\",\n \"payload_on\": \"home\",\n \"payload_off\": \"not_home\",\n \"device_class\": \"presence\",\n \"device\": {\n \"identifiers\": [\"spaxel_{mothership_id}\"],\n \"name\": \"Spaxel\",\n \"model\": \"Spaxel Presence System\",\n \"manufacturer\": \"Spaxel\"\n }\n}\n\nPer zone: sensor (occupancy count) + binary_sensor (zone occupied)\nDiscovery topic: homeassistant/sensor/spaxel_{mothership_id}_{zone_id}_occupancy/config\nPayload: {name, unique_id, state_topic, unit_of_measurement: \"people\", device_class: null, device: ...}\n\nFall detection: binary_sensor with device_class: \"safety\"\nDiscovery topic: homeassistant/binary_sensor/spaxel_{mothership_id}_fall/config\n\nSystem mode: select (home/away/sleep) or input_select via MQTT\nDiscovery + command topics so HA can set system mode.\n\nWhen a person or zone is deleted, publish an empty payload to the corresponding discovery topic (HA treats this as \"remove entity\").\n\n## Event Publishing\n\nThe MQTT client subscribes to the internal event bus and publishes:\n- ZoneCrossingEvent -> update zone occupancy topics\n- PersonPresenceChangeEvent -> update person presence topic\n- FallEvent -> publish to fall_detected topic\n- SystemHealthEvent -> publish to system health topic (60s interval)\n\n## Generic Webhook Integration\n\nA complementary feature: a system-level webhook that delivers ALL spaxel events to a single user-configured URL, not just user-configured automations. This is for integrations that want to receive the full event stream (e.g. a custom backend, a logging service, or a complex HA automation that can't be expressed in the automation builder).\n\nPOST /api/settings/system-webhook: set URL\nAll internal events are serialised as JSON and POST'd to this URL with event type in the X-Spaxel-Event header.\nSame retry policy as automation webhooks (one retry after 30s on 5xx).\n\n## Dashboard Settings\n\nSettings panel -> \"Integrations\" tab:\n- MQTT section: broker URL, username, password (masked), TLS toggle, \"Test Connection\" button, \"Publish discovery payloads now\" button\n- System webhook section: URL input, \"Test\" button, recent delivery log (last 10 events with status)\n- Status indicator: green dot if MQTT connected, red if disconnected with last error message\n\n## Tests\n\n- Test MQTT connection to a local broker (use go-mqtt-broker test dependency or a Docker Eclipse Mosquitto in CI)\n- Test auto-discovery payload format against HA MQTT auto-discovery spec (compare JSON structure)\n- Test retained messages: verify that on reconnect, retained messages are not re-published if unchanged\n- Test that fall_detected topic is published on FallEvent\n- Test that zone occupancy topics update correctly on ZoneCrossingEvent\n- Test auto-discovery cleanup: deleting a person sends empty payload to discovery topic\n- Test system webhook delivers all event types to a mock HTTP server\n- Test reconnect policy: simulate broker disconnect, verify client reconnects with backoff\n\n## Acceptance Criteria\n\n- Spaxel entities appear automatically in Home Assistant entity list after MQTT config without any HA config editing\n- Person presence binary_sensor in HA updates within 30 seconds of BLE presence change\n- Zone occupancy sensor in HA reflects correct integer count\n- Fall detection binary_sensor fires and resets correctly\n- System mode select allows bidirectional control from HA (HA can set spaxel mode via MQTT)\n- System webhook receives all event types\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-03-28T01:49:02.414667027Z","created_by":"coding","updated_at":"2026-04-11T10:36:18.940696600Z","closed_at":"2026-04-11T10:36:18.940634302Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:7"],"dependencies":[{"issue_id":"spaxel-tgj","depends_on_id":"spaxel-c0q","type":"blocks","created_at":"2026-03-28T03:29:14.412533268Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tgj","depends_on_id":"spaxel-c1c","type":"blocks","created_at":"2026-03-28T01:49:05.719934686Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tgj","depends_on_id":"spaxel-nqh","type":"blocks","created_at":"2026-03-28T01:49:05.665936722Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tgj","depends_on_id":"spaxel-qlh","type":"blocks","created_at":"2026-03-28T01:49:05.694421101Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-tig","title":"Guided troubleshooting (enhanced for production)","description":"## Background\n\nThe Phase 4 guided troubleshooting bead (spaxel-r0l) covers onboarding failures and basic node offline recovery. This Phase 9 bead extends it with production-quality proactive assistance that emerges from the system's own understanding of its state: detection quality degradation warnings with root-cause analysis, post-feedback explanations that close the loop for curious users, feature discovery notifications that teach users about new capabilities as they become available, and a searchable help system for self-service support.\n\nThe philosophy: the system should feel like a knowledgeable, helpful companion that notices problems before you do and explains them in terms you understand.\n\n## 1. Proactive Quality Prompts\n\nTrigger: when ambient confidence score (Phase 5, spaxel-sbi) drops below 0.6 on any link for more than 5 consecutive minutes.\n\nAction: show a non-blocking, dismissible prompt card in the dashboard:\n\"Detection quality has dropped on [Node A] to [Node B]. This link is at [N]% reliability.\"\n\nThe card includes:\n- A \"Diagnose\" button that runs the link weather diagnostics (Phase 5, spaxel-32o) and shows the results in a slide-out panel.\n- \"Dismiss for today\" option (hides until tomorrow or until quality recovers and drops again)\n- The 3D link line is highlighted (pulsing amber) while the prompt is active\n\nThe link weather diagnostic result is shown in plain English:\n- \"Possible cause: new furniture may be blocking the sensing zone. [See where to move the node]\"\n- \"Possible cause: WiFi congestion from neighbouring networks. [What to do about this]\"\n- \"Cause unknown — system is adapting. [No action needed]\"\n\nDo NOT show this prompt if the quality drop is temporary (< 5 minutes). Many quality drops are transient (microwave oven use, brief WiFi congestion) and showing a prompt for every one would train the user to ignore them.\n\n## 2. Repeated-Setting Change Detection\n\nTrigger: if a user changes the same configuration setting (motion threshold, baseline tau, etc.) more than 3 times within a 24-hour period.\n\nThis is a signal that the user is struggling to find the right value. The system should proactively offer help rather than silently watch the user thrash.\n\nAction: show a non-intrusive prompt: \"Looks like you're fine-tuning the motion threshold. Would you like help finding the right value for your space?\"\n\nThe \"Help me tune this\" button opens a guided calibration flow:\n1. \"Set threshold too low?\" -> walk around room, system shows how many false positives at the current setting\n2. \"Set threshold too high?\" -> sit still, system shows if motion is being missed\n3. Suggests optimal value based on the diurnal baseline SNR and link health\n4. \"Apply suggested value: [N]\" button\n\nTrack repeated changes in localStorage: {setting_name: {count, last_change_timestamp}} for the last 24h.\n\n## 3. Post-Feedback Explanations\n\nWhen a user marks a detection event as a false positive (FALSE_POSITIVE feedback from spaxel-3ps), show an explanation:\n\n\"The system detected motion here because: [link A to link B]'s signal (deltaRMS) exceeded the motion threshold by [N]x at [time]. This could be caused by: [root cause from diagnostic rules, if any match, or 'ambient RF interference' as default]. We've noted this and will apply a correction.\"\n\nThe explanation uses the ExplainabilitySnapshot for the event (spaxel-ez4) to identify the contributing link, and runs a quick diagnostic check (spaxel-32o rule engine) on the link's health history at the event timestamp.\n\nShow as an expandable section (\"Why did this happen?\") on the feedback confirmation card, not as a separate notification.\n\n## 4. Feature Discovery Notifications\n\nWhen a feature becomes available for the first time (requires a one-time condition to be met), fire a single non-blocking notification:\n\nEvents and their messages:\n- DiurnalBaselineActivated (after 7 days of data, Phase 5): \"Your system has learned your home's daily patterns. Detection accuracy should improve starting today.\"\n- FirstSleepSessionComplete (first sleep monitored, Phase 7): \"Your first sleep session was tracked overnight. Tap to see your sleep summary.\"\n- WeightUpdateApproved (first self-improving weight update, Phase 7): \"Localisation accuracy improved based on your BLE device positions. Median position error decreased.\"\n- AutomationFirstFired (first automation triggered, Phase 6): \"Your first automation just ran! [View in automations log]\"\n- PredictionModelReady (7 days per person, Phase 7): \"Presence predictions are now available for [person name]. [View predictions]\"\n\nEach notification:\n- Keyed by a unique event ID stored in SQLite: feature_notifications (event_id TEXT PRIMARY KEY, fired_at DATETIME, acknowledged_at DATETIME)\n- Never fires twice (primary key ensures uniqueness)\n- Dismissed by tapping (marks acknowledged_at)\n- Does not fire during quiet hours (uses notification manager quiet hours logic)\n\n## 5. Contextual Help System\n\nA \"?\" button in the expert mode header opens a searchable help overlay. The overlay contains:\n- A search input (same fuzzy search as command palette, Phase 9 spaxel-tvq)\n- A list of help articles (approximately 30 covering all major features)\n- Each article: title, 1-3 sentence plain-English explanation, link to a relevant dashboard section or action\n\nSample articles:\n- \"What is a sensing link?\" — \"A sensing link is the path between two spaxel nodes (one transmitting, one receiving). Motion in the space between them changes the signal, which spaxel detects.\"\n- \"Why is my detection quality low?\" — \"Low quality usually means interference from other WiFi devices, an obstacle in the sensing zone, or the node is too far from your router. Click 'Diagnose' on the link to find the specific cause.\"\n- \"How does presence prediction work?\" — \"After 7 days, spaxel learns when people are typically in each room and predicts where they'll be next. Predictions appear in the Predictions panel.\"\n- \"What is the Fresnel zone?\" — \"The Fresnel zone is the region in space where motion has the most impact on the signal between two nodes. Spaxel uses this geometry to estimate where in the room a person is.\"\n\nArticles are stored as a static JSON file (dashboard/help_articles.json, 30-50 articles). No server round-trip needed for help.\n\n## Implementation Structure\n\n- mothership/internal/help/notifier.go: feature discovery notification manager, persistence\n- dashboard/js/proactive.js: quality prompt, repeated-setting detector, post-feedback explanation\n- dashboard/js/help.js: help overlay, fuzzy search on articles, article rendering\n- dashboard/help_articles.json: all help article content\n- mothership/internal/diagnostics/linkweather.go: add GetDiagnosticFor(linkID, timestamp) method for post-feedback use\n\n## Tests\n\n- Test quality prompt trigger: mock confidence score = 0.55 for 6 minutes, verify prompt appears; confidence = 0.55 for 3 minutes, verify no prompt\n- Test repeated-setting detection: mock 4 threshold changes in 20h, verify help prompt fires\n- Test post-feedback explanation: inject FALSE_POSITIVE feedback on an event with a known ExplainabilitySnapshot, verify explanation text contains the contributing link name\n- Test feature discovery: inject DiurnalBaselineActivated event, verify notification fires once; inject again, verify no second notification\n- Test help search: query \"fresnel\" -> \"What is the Fresnel zone?\" appears in results\n- Test dismissed prompts don't re-appear (localStorage + SQLite persistence)\n\n## Acceptance Criteria\n\n- Quality prompt appears within 5 minutes of confidence score dropping below 0.6 threshold\n- Prompt does NOT appear for transient drops shorter than 5 minutes\n- \"Diagnose\" in quality prompt shows root cause in plain language\n- Repeated-setting detection fires after 3+ changes in 24h\n- Post-feedback explanation shown after any FALSE_POSITIVE feedback submission\n- Feature discovery notifications fire exactly once per feature, in plain language\n- Help overlay opens with \"?\" button, search finds relevant articles\n- Dismissed prompts never re-appear (unless condition reoccurs after recovery)\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"open","priority":3,"issue_type":"task","owner":"","created_at":"2026-03-28T02:04:15.981731936Z","created_by":"coding","updated_at":"2026-04-11T03:34:49.204992554Z","close_reason":"","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"],"dependencies":[{"issue_id":"spaxel-tig","depends_on_id":"spaxel-32o","type":"blocks","created_at":"2026-03-28T02:04:22.625685956Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-dz5s","type":"blocks","created_at":"2026-04-11T03:34:48.997943090Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-kgn4","type":"blocks","created_at":"2026-04-11T03:34:49.167476389Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-qaaa","type":"blocks","created_at":"2026-04-11T03:34:49.204953181Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-sbi","type":"blocks","created_at":"2026-03-28T02:04:22.592483085Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:15.089304486Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-v1ep","type":"blocks","created_at":"2026-04-11T03:34:49.052040423Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-x6jb","type":"blocks","created_at":"2026-04-11T03:34:49.115061838Z","created_by":"coding","thread_id":""}]} +{"id":"spaxel-tig","title":"Guided troubleshooting (enhanced for production)","description":"## Background\n\nThe Phase 4 guided troubleshooting bead (spaxel-r0l) covers onboarding failures and basic node offline recovery. This Phase 9 bead extends it with production-quality proactive assistance that emerges from the system's own understanding of its state: detection quality degradation warnings with root-cause analysis, post-feedback explanations that close the loop for curious users, feature discovery notifications that teach users about new capabilities as they become available, and a searchable help system for self-service support.\n\nThe philosophy: the system should feel like a knowledgeable, helpful companion that notices problems before you do and explains them in terms you understand.\n\n## 1. Proactive Quality Prompts\n\nTrigger: when ambient confidence score (Phase 5, spaxel-sbi) drops below 0.6 on any link for more than 5 consecutive minutes.\n\nAction: show a non-blocking, dismissible prompt card in the dashboard:\n\"Detection quality has dropped on [Node A] to [Node B]. This link is at [N]% reliability.\"\n\nThe card includes:\n- A \"Diagnose\" button that runs the link weather diagnostics (Phase 5, spaxel-32o) and shows the results in a slide-out panel.\n- \"Dismiss for today\" option (hides until tomorrow or until quality recovers and drops again)\n- The 3D link line is highlighted (pulsing amber) while the prompt is active\n\nThe link weather diagnostic result is shown in plain English:\n- \"Possible cause: new furniture may be blocking the sensing zone. [See where to move the node]\"\n- \"Possible cause: WiFi congestion from neighbouring networks. [What to do about this]\"\n- \"Cause unknown — system is adapting. [No action needed]\"\n\nDo NOT show this prompt if the quality drop is temporary (< 5 minutes). Many quality drops are transient (microwave oven use, brief WiFi congestion) and showing a prompt for every one would train the user to ignore them.\n\n## 2. Repeated-Setting Change Detection\n\nTrigger: if a user changes the same configuration setting (motion threshold, baseline tau, etc.) more than 3 times within a 24-hour period.\n\nThis is a signal that the user is struggling to find the right value. The system should proactively offer help rather than silently watch the user thrash.\n\nAction: show a non-intrusive prompt: \"Looks like you're fine-tuning the motion threshold. Would you like help finding the right value for your space?\"\n\nThe \"Help me tune this\" button opens a guided calibration flow:\n1. \"Set threshold too low?\" -> walk around room, system shows how many false positives at the current setting\n2. \"Set threshold too high?\" -> sit still, system shows if motion is being missed\n3. Suggests optimal value based on the diurnal baseline SNR and link health\n4. \"Apply suggested value: [N]\" button\n\nTrack repeated changes in localStorage: {setting_name: {count, last_change_timestamp}} for the last 24h.\n\n## 3. Post-Feedback Explanations\n\nWhen a user marks a detection event as a false positive (FALSE_POSITIVE feedback from spaxel-3ps), show an explanation:\n\n\"The system detected motion here because: [link A to link B]'s signal (deltaRMS) exceeded the motion threshold by [N]x at [time]. This could be caused by: [root cause from diagnostic rules, if any match, or 'ambient RF interference' as default]. We've noted this and will apply a correction.\"\n\nThe explanation uses the ExplainabilitySnapshot for the event (spaxel-ez4) to identify the contributing link, and runs a quick diagnostic check (spaxel-32o rule engine) on the link's health history at the event timestamp.\n\nShow as an expandable section (\"Why did this happen?\") on the feedback confirmation card, not as a separate notification.\n\n## 4. Feature Discovery Notifications\n\nWhen a feature becomes available for the first time (requires a one-time condition to be met), fire a single non-blocking notification:\n\nEvents and their messages:\n- DiurnalBaselineActivated (after 7 days of data, Phase 5): \"Your system has learned your home's daily patterns. Detection accuracy should improve starting today.\"\n- FirstSleepSessionComplete (first sleep monitored, Phase 7): \"Your first sleep session was tracked overnight. Tap to see your sleep summary.\"\n- WeightUpdateApproved (first self-improving weight update, Phase 7): \"Localisation accuracy improved based on your BLE device positions. Median position error decreased.\"\n- AutomationFirstFired (first automation triggered, Phase 6): \"Your first automation just ran! [View in automations log]\"\n- PredictionModelReady (7 days per person, Phase 7): \"Presence predictions are now available for [person name]. [View predictions]\"\n\nEach notification:\n- Keyed by a unique event ID stored in SQLite: feature_notifications (event_id TEXT PRIMARY KEY, fired_at DATETIME, acknowledged_at DATETIME)\n- Never fires twice (primary key ensures uniqueness)\n- Dismissed by tapping (marks acknowledged_at)\n- Does not fire during quiet hours (uses notification manager quiet hours logic)\n\n## 5. Contextual Help System\n\nA \"?\" button in the expert mode header opens a searchable help overlay. The overlay contains:\n- A search input (same fuzzy search as command palette, Phase 9 spaxel-tvq)\n- A list of help articles (approximately 30 covering all major features)\n- Each article: title, 1-3 sentence plain-English explanation, link to a relevant dashboard section or action\n\nSample articles:\n- \"What is a sensing link?\" — \"A sensing link is the path between two spaxel nodes (one transmitting, one receiving). Motion in the space between them changes the signal, which spaxel detects.\"\n- \"Why is my detection quality low?\" — \"Low quality usually means interference from other WiFi devices, an obstacle in the sensing zone, or the node is too far from your router. Click 'Diagnose' on the link to find the specific cause.\"\n- \"How does presence prediction work?\" — \"After 7 days, spaxel learns when people are typically in each room and predicts where they'll be next. Predictions appear in the Predictions panel.\"\n- \"What is the Fresnel zone?\" — \"The Fresnel zone is the region in space where motion has the most impact on the signal between two nodes. Spaxel uses this geometry to estimate where in the room a person is.\"\n\nArticles are stored as a static JSON file (dashboard/help_articles.json, 30-50 articles). No server round-trip needed for help.\n\n## Implementation Structure\n\n- mothership/internal/help/notifier.go: feature discovery notification manager, persistence\n- dashboard/js/proactive.js: quality prompt, repeated-setting detector, post-feedback explanation\n- dashboard/js/help.js: help overlay, fuzzy search on articles, article rendering\n- dashboard/help_articles.json: all help article content\n- mothership/internal/diagnostics/linkweather.go: add GetDiagnosticFor(linkID, timestamp) method for post-feedback use\n\n## Tests\n\n- Test quality prompt trigger: mock confidence score = 0.55 for 6 minutes, verify prompt appears; confidence = 0.55 for 3 minutes, verify no prompt\n- Test repeated-setting detection: mock 4 threshold changes in 20h, verify help prompt fires\n- Test post-feedback explanation: inject FALSE_POSITIVE feedback on an event with a known ExplainabilitySnapshot, verify explanation text contains the contributing link name\n- Test feature discovery: inject DiurnalBaselineActivated event, verify notification fires once; inject again, verify no second notification\n- Test help search: query \"fresnel\" -> \"What is the Fresnel zone?\" appears in results\n- Test dismissed prompts don't re-appear (localStorage + SQLite persistence)\n\n## Acceptance Criteria\n\n- Quality prompt appears within 5 minutes of confidence score dropping below 0.6 threshold\n- Prompt does NOT appear for transient drops shorter than 5 minutes\n- \"Diagnose\" in quality prompt shows root cause in plain language\n- Repeated-setting detection fires after 3+ changes in 24h\n- Post-feedback explanation shown after any FALSE_POSITIVE feedback submission\n- Feature discovery notifications fire exactly once per feature, in plain language\n- Help overlay opens with \"?\" button, search finds relevant articles\n- Dismissed prompts never re-appear (unless condition reoccurs after recovery)\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-03-28T02:04:15.981731936Z","created_by":"coding","updated_at":"2026-05-04T08:23:55.721670493Z","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:1"],"dependencies":[{"issue_id":"spaxel-tig","depends_on_id":"spaxel-32o","type":"blocks","created_at":"2026-03-28T02:04:22.625685956Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-dz5s","type":"blocks","created_at":"2026-04-11T03:34:48.997943090Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-kgn4","type":"blocks","created_at":"2026-04-11T03:34:49.167476389Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-qaaa","type":"blocks","created_at":"2026-04-11T03:34:49.204953181Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-sbi","type":"blocks","created_at":"2026-03-28T02:04:22.592483085Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:15.089304486Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-v1ep","type":"blocks","created_at":"2026-04-11T03:34:49.052040423Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-tig","depends_on_id":"spaxel-x6jb","type":"blocks","created_at":"2026-04-11T03:34:49.115061838Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-tim","title":"Adaptive sensing rate","description":"Implement mothership-controlled sensing rate with on-device burst detection.\n\n## Deliverables\n- Mothership rate control: send rate change commands (2Hz idle ↔ 50Hz active) per link via WebSocket\n- Rate decision logic: drop to 2Hz when no motion for 30s, burst to 50Hz on motion\n- On-device amplitude variance check at low rate (2Hz) — if variance exceeds threshold, burst to full rate and notify mothership\n- Motion hints from ESP32 to preemptively ramp adjacent links\n- Firmware changes: firmware/main/csi.c to support dynamic rate changes\n\n## Acceptance Criteria\n- Idle links automatically drop to 2Hz packet rate\n- Motion triggers burst to configured active rate\n- ESP32 can locally detect motion onset and self-burst before mothership commands\n- Adjacent links ramp up on motion hints from nearby nodes\n- Tests for rate decision logic in mothership\n\n## References\n- Plan: docs/plan/plan.md item 12\n- CSI capture: firmware/main/csi.c\n- Signal pipeline: mothership/internal/signal/processor.go","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","owner":"","created_at":"2026-03-27T01:56:21.876231481Z","created_by":"coding","updated_at":"2026-03-28T05:36:02.592406514Z","closed_at":"2026-03-28T05:36:02.592346365Z","close_reason":"Implemented: ratecontrol.go (bcfd1e3, fb69190) — mothership-controlled 2Hz↔50Hz rate changes, firmware csi.c/websocket.c dynamic rate config, on-device amplitude variance burst detection","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} {"id":"spaxel-tqj","title":"CSI recording buffer","description":"Implement disk-backed circular buffer for CSI frame recording.\n\n## Deliverables\n- New package: mothership/internal/recording/\n- Append incoming CSI frames to disk-backed circular buffer (48h default retention)\n- Binary format for efficient storage (same frame format as WebSocket)\n- Configurable retention period via environment variable\n- Foundation for time-travel replay feature (Phase 8)\n\n## Acceptance Criteria\n- CSI frames are persisted to disk as they arrive\n- Buffer auto-prunes frames older than retention period\n- Can read back frames for a given time range\n- Storage is bounded (auto-prune prevents disk exhaustion)\n- Tests cover write, read-back, and pruning\n\n## References\n- Frame format: mothership/internal/ingestion/frame.go (24-byte header + payload)\n- Ring buffer: mothership/internal/ingestion/ring.go (in-memory reference)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","owner":"","created_at":"2026-03-27T01:56:09.947974130Z","created_by":"coding","updated_at":"2026-03-28T01:34:05.658150061Z","closed_at":"2026-03-27T03:02:15.596740568Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-tqj","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T01:34:05.658124716Z","created_by":"coding","thread_id":""}]} {"id":"spaxel-tr7","title":"3D spatial visualization","description":"Full Three.js 3D scene with humanoid figures, room bounds, and node meshes.\n\n## Deliverables\n- Room bounds visualization (walls, floor, ceiling from configured dimensions)\n- Floor plan texture support (uploaded image mapped to ground plane)\n- Humanoid figures using SkinnedMesh + AnimationMixer (standing/walking/seated/lying)\n- Vertical pillar anchors and footprint trails for tracked blobs\n- Node meshes at configured 3D positions with link lines between pairs\n- View presets (top-down, perspective, first-person follow)\n- WebSocket integration to receive blob positions from mothership\n\n## Acceptance Criteria\n- Humanoid figures animate smoothly between postures\n- User can orbit, pan, zoom with OrbitControls\n- Node positions and link lines update in real-time\n- Works with existing dashboard skeleton (dashboard/js/app.js)\n\n## References\n- Plan: docs/plan/plan.md item 17\n- Dashboard: dashboard/index.html, dashboard/js/app.js","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","owner":"","created_at":"2026-03-27T01:57:04.504533558Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.148595152Z","closed_at":"2026-03-28T05:36:26.148493523Z","close_reason":"Implemented: dashboard/js/viz3d.js 566 lines (bcd19ad) — room bounds, humanoid SkinnedMesh 13-bone skeleton 4 postures, footprint trails, node meshes, link lines, 3 view presets","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"],"dependencies":[{"issue_id":"spaxel-tr7","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T03:29:13.740185994Z","created_by":"coding","thread_id":""}]} diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index b07f0fd..76e523f 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -9e0ce4a8270e0cd0c8d0d5255fe5c09c165988fd +1845c09bb150f9e790f747c19af0f1a488fbee61 diff --git a/mothership/mothership b/mothership/mothership index e92a422..d43ae2c 100755 Binary files a/mothership/mothership and b/mothership/mothership differ diff --git a/mothership/sim b/mothership/sim index e2c712b..eb376ee 100755 Binary files a/mothership/sim and b/mothership/sim differ