diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 9a91082..33c65bb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,252 +1,83 @@ -{"id":"bf-13gwk","title":"Spatial quick actions (Component 32)","description":"## Goal\nRight-click (desktop) or long-press (mobile) anywhere in 3D view to get context-sensitive actions based on what's under cursor.\n\n## Scope\nActions per target:\n- On blob: 'Who is this?' (BLE assignment), 'Why is this here?' (explainability), 'Follow' (camera tracks), 'Create automation here', 'Mark incorrect' (thumbs-down), 'Track history'\n- On node: 'Diagnostics' (CSI plot), 'Blink LED', 'Reposition', 'Update firmware', 'Show links', 'Disable/Enable'\n- On empty floor space: 'What happened here?' (filter timeline), 'Add trigger zone', 'Add virtual node', 'Coverage quality'\n- On zone label: 'Zone history', 'Edit zone', 'Create automation', 'Crowd flow'\n- On portal: 'Crossing log', 'Edit portal', 'Reverse direction'\n- On trigger volume: 'Edit trigger', 'Test', 'View log', 'Disable/Enable'\n\n## Implementation\nThree.js Raycaster determines what's under cursor\nSingle context menu component renders appropriate options\nEach action dispatches to existing dashboard functions (no new backend endpoints needed)\n\n## Acceptance\n- Right-click/long-press on blob shows blob-specific actions\n- Right-click/long-press on node shows node-specific actions\n- Right-click/long-press on empty space shows space-specific actions\n- Actions execute correctly\n- Works on both desktop (right-click) and mobile (long-press)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.548191949Z","updated_at":"2026-05-06T08:32:57.207248413Z","closed_at":"2026-05-06T08:32:57.207248413Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-1ih7k","title":"TX slot collision detection and adaptive re-stagger","description":"Plan (Component 5 / Fleet Manager) specifies collision monitoring: if CSI frames from two TX nodes arrive within 3ms of each other, log a 'possible slot collision' metric. If collision rate > 5% over a 60-second window, re-randomize stagger assignments (shift one node's slot by half a slot width) and push updated config messages. The fleet manager computes stagger slots but has no collision detection, no re-stagger logic, and no collision rate metric. Needs: (1) per-link-pair collision counter in ingestion/signal processing path, (2) collision rate aggregation in fleet manager, (3) adaptive re-stagger trigger.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:13.899248435Z","updated_at":"2026-05-06T12:29:01.414753570Z","closed_at":"2026-05-06T12:29:01.414753570Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-1k3zg","title":"API: GET /api/doctor — pre-flight configuration diagnostic","description":"## Goal\nAdd a GET /api/doctor endpoint that diagnoses common misconfiguration before the user concludes the system is broken. Complements /healthz (runtime state) with pre-flight checks (configuration correctness).\n\n## Endpoint\n\nGET /api/doctor\n- Requires session cookie (same as all /api/* endpoints)\n- Returns 200 with a JSON report regardless of check results (HTTP status reflects reachability, not check results)\n\n## Checks to run\n\n| Check | Pass condition | Fail message |\n|---|---|---|\n| data_dir_writable | /data is writable and has >100 MB free | 'Data directory not writable' or 'Disk space low: Nf MB free' |\n| db_integrity | PRAGMA integrity_check returns 'ok' | 'SQLite integrity check failed' |\n| firmware_dir | /firmware contains at least one *.bin file | 'No firmware binaries found — OTA updates unavailable' |\n| mdns_binding | mDNS service is registered (or SPAXEL_MDNS_ENABLED=false) | 'mDNS not advertising — nodes cannot auto-discover mothership' |\n| mqtt_reachable | If SPAXEL_MQTT_BROKER is set: TCP connect to broker succeeds within 3s | 'MQTT broker unreachable: ' |\n| ntp_reachable | UDP ping to SPAXEL_NTP_SERVER:123 resolves within 3s | 'NTP server unreachable — node clock sync may fail' |\n| install_secret | install_secret row exists in auth table | 'Installation secret missing — re-run container to regenerate' |\n| pin_configured | pin_bcrypt is non-null in auth table | 'Dashboard PIN not configured — run first-time setup' |\n| node_token_consistency | All nodes in registry have non-null node_token | 'N nodes missing auth tokens — re-provision via Web Serial' |\n\n## Response format\n\n{\n 'checks': [\n {'name': 'db_integrity', 'status': 'ok', 'message': null},\n {'name': 'mqtt_reachable', 'status': 'warn', 'message': 'MQTT broker unreachable: mqtt://ha.local:1883'},\n {'name': 'firmware_dir', 'status': 'error', 'message': 'No firmware binaries found'}\n ],\n 'overall': 'warn', // 'ok' | 'warn' | 'error' (worst of all checks)\n 'checked_at': '2024-03-15T07:00:00Z'\n}\n\nStatus levels: 'ok' (pass), 'warn' (degraded but functional), 'error' (action required).\n\n## Dashboard integration\n- Command palette: 'doctor' → calls /api/doctor, shows results inline\n- Guided troubleshooting (Component 36): 'Node offline' flow links to 'Run diagnostics' which calls /api/doctor\n- /healthz already covers runtime health; /api/doctor covers configuration health — keep them separate\n\n## Acceptance\n- GET /api/doctor returns 200 with all checks when fully configured\n- Reports 'firmware_dir: error' when /firmware is empty\n- Reports 'mqtt_reachable: warn' when MQTT broker env is set but broker is unreachable\n- Unit tests cover each check in isolation with mocked dependencies","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:22:51.188946318Z","updated_at":"2026-05-05T12:39:28.566216915Z","closed_at":"2026-05-05T12:39:28.566216915Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-1t0kn","title":"OTA auto-update with canary strategy and quiet window","description":"Plan specifies a canary OTA strategy (Component 6): update one node first, monitor quality for 10 min, then roll out fleet-wide. Also needs a configurable quiet window (default 02:00–05:00 local) and auto-update mode toggle. Currently the fleet manager only does manual rolling OTA — no canary logic, no scheduled quiet window, no auto-update-on-firmware-detect. Implementation needed in internal/ota and/or fleet manager with a settings key for auto_update_enabled, quiet_window, canary_duration_min.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:24:59.888109951Z","updated_at":"2026-05-05T16:42:25.163065452Z","closed_at":"2026-05-05T16:42:25.163065452Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-232u3","title":"GET /api/notifications/preview — rendered test thumbnail endpoint","description":"Plan (Component 30, Renderer spec) specifies a test endpoint: GET /api/notifications/preview?type=fall&person=Alice returns a rendered test image for UI development and QA. The render package (internal/render/floorplan.go) implements thumbnail generation with fogleman/gg, but the preview HTTP endpoint is never registered in main.go. Needs: handler that accepts ?type and ?person query params, calls the appropriate Generate*Thumbnail function, returns the PNG bytes with Content-Type: image/png.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:20.897907993Z","updated_at":"2026-05-05T17:25:10.053618969Z","closed_at":"2026-05-05T17:25:10.053618969Z","close_reason":"GET /api/notifications/preview endpoint already implemented and registered in commit 37571ec. The endpoint accepts ?type (fall, anomaly, zone_enter, sleep) and ?person query params, calls the appropriate Generate*Thumbnail function from internal/render/floorplan.go, and returns PNG bytes with Content-Type: image/png. All tests pass.","source_repo":".","compaction_level":0} -{"id":"bf-25dmx","title":"Guided troubleshooting (Component 36)","description":"## Goal\nWhen system detects that user might be struggling or detection quality has degraded, proactively offer contextual help — but never when things are working well.\n\n## Trigger conditions and responses:\n\nDetection quality drops:\n- Condition: Zone-level detection quality below 60% for >24 hours\n- Banner in timeline and 3D view: 'Detection in the kitchen has been less reliable this week. Want me to help diagnose?'\n- Guided flow: Check node connectivity → show link health with explainability → suggest node repositioning → offer re-baseline → 'Still not right? Try adding a node here [highlighted position]'\n\nRepeated setting changes:\n- Condition: Same settings key modified 3+ times within 60-minute sliding window (qualifying keys: delta_rms_threshold, breathing_sensitivity, tau_s, fresnel_decay, n_subcarriers)\n- Tracking: per-key edit counter in memory, resets after 60 min inactivity\n- Trigger: when counter reaches 3, set hint_pending flag\n- Frontend: show non-intrusive banner: 'You've adjusted the detection threshold several times. Would you like me to show you what the system is seeing?' with [Show me] and [×] dismiss\n- [Show me]: opens time-travel to most recent detection event before first edit, with explainability overlay pre-activated\n- Cooldown: 24 hours after hint is shown\n\nNode offline:\n- Condition: Any node offline for >2 hours\n- Timeline event with expandable troubleshooting steps\n\nFirst-time feature discovery:\n- Condition: User opens feature panel for first time\n- Brief, non-intrusive tooltip (not modal): 'Draw a box around an area, then choose what happens when someone enters or leaves. [Got it]'\n- Shown once, never repeated\n\nAfter false positive feedback:\n- Inline response in timeline: 'Got it. I've slightly raised the detection threshold for the contributing links. If this keeps happening at this time of day, my hourly baseline will adapt within a few days.'\n\nAfter successful calibration:\n- Positive reinforcement: 'Re-baseline complete. Detection quality in the kitchen improved from 64% to 89%.'\n\n## Design principles\n- Reactive, not proactive: help appears only when something seems wrong or when user is clearly exploring\n- Dismissible in one tap: never blocks UI\n- Never repeats after dismissal (stored in localStorage)\n- Always explains what will happen next\n- Never condescending: assumes user is intelligent but may not know CSI physics\n\n## Acceptance\n- Detection quality drop triggers helpful banner\n- Repeated setting changes trigger hint\n- Node offline shows troubleshooting steps\n- First-time feature discovery shows tooltip once\n- Feedback responses are helpful\n- Calibration success shows positive reinforcement","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.724435180Z","updated_at":"2026-05-06T10:19:23.187979577Z","closed_at":"2026-05-06T10:19:23.187979577Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"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":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.529140576Z","updated_at":"2026-05-06T08:26:57.944384490Z","closed_at":"2026-05-06T08:26:57.944384490Z","close_reason":"Completed","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 --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} -{"id":"bf-3gr58","title":"Simple mode (progressive disclosure)","description":"## Goal\nCard-based mobile-first UI for household members who don't need the full 3D engineering view.\n\n## Scope\n- No 3D scene — replaces with responsive card layout\n- Room cards: one per defined zone, shows occupancy count, person names (if BLE-identified), status color\n- Activity feed: chronological list of events (from timeline), tap to expand\n- Alert banner: fall detection, anomaly alerts, system warnings\n- Quick actions: arm/disarm security mode, trigger re-baseline, silence alerts\n- Sleep summary card: morning card showing last night's sleep data\n- Mobile-first: touch-friendly, no gestures required\n- Switching: toggle button in toolbar, per-user default stored in localStorage\n- Optional: simple mode requires no auth, expert mode requires PIN\n\n## Location\ndashboard/simple.html (new route)\ndashboard/static/js/simple-mode.js (new module)\n\n## Acceptance\n- Non-technical user can check occupancy without training\n- Room cards show current status with color coding\n- Activity feed shows recent events\n- Toggle between simple/expert mode works\n- Mobile-responsive layout","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.506623704Z","updated_at":"2026-05-06T07:05:30.077682950Z","closed_at":"2026-05-06T07:05:30.077682950Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-3h1hk","title":"Playwright e2e acceptance scenarios AS-1 through AS-6","description":"Open bead bf-3a2py tracks acceptance scenario integration tests but they're not implemented. The tests/e2e/run.sh exists as a bash harness skeleton. The plan implies specific acceptance scenarios covering the full happy path: (AS-1) first-node provisioning → presence detection in <30s; (AS-2) multi-node localization accuracy; (AS-3) portal crossing detection; (AS-4) fall detection alert chain; (AS-5) OTA update flow; (AS-6) security mode arm/alert/disarm. These should be Playwright tests against a running mothership+sim stack.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:26:08.138470605Z","updated_at":"2026-05-02T18:26:38.599373498Z","closed_at":"2026-05-02T18:26:38.599373498Z","close_reason":"Duplicate of existing open bead bf-3a2py which already tracks acceptance scenario integration tests AS-1 through AS-6","source_repo":".","compaction_level":0} -{"id":"bf-3h5kd","title":"golangci-lint config and CI gate for mothership","description":"The open bead bf-w15bj tracks a CI pipeline gate for golangci-lint, but no .golangci.yml exists in the repo root or mothership/ directory. The sim Makefile references golangci-lint but only for cmd/sim. The main mothership codebase (~50+ packages) has no lint config. Needed: a .golangci.yml at mothership/ level with appropriate linter set (errcheck, staticcheck, unused, govet, etc.) and a CI step in the Argo WorkflowTemplate mta-my-way-build (or a separate lint-only workflow).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:25:46.332413911Z","updated_at":"2026-05-02T18:26:44.610380579Z","closed_at":"2026-05-02T18:26:44.610380579Z","close_reason":"Duplicate of existing open bead bf-w15bj which already tracks golangci-lint CI gate with full config specification","source_repo":".","compaction_level":0} -{"id":"bf-3jv1x","title":"Complete portal crossings GET /api/portals/:id/crossings endpoint","description":"Plan's REST API spec defines GET /api/portals/:id/crossings with ?limit and ?before cursor pagination, returning [{timestamp, direction, person, blob_id}]. The portals package and CRUD endpoints exist (internal/api/zones.go handles portals), and portal_crossings is in the SQLite schema, but the crossings query endpoint is not implemented. This is needed for the timeline 'crossing log' quick action and for the portal detail view.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:57.312746596Z","updated_at":"2026-05-05T19:35:15.900705949Z","closed_at":"2026-05-05T19:35:15.900705949Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-3p4bj","title":"Command palette (Component 34)","description":"## Goal\nCtrl+K (Cmd+K on Mac) opens universal search and command interface. Invisible to casual users, indispensable for power users.\n\n## Scope\nSearch:\n- 'kitchen' → Kitchen zone, kitchen nodes, kitchen automations, recent kitchen events\n- 'alice' → Alice's current location, today's timeline, sleep report, BLE devices\n- 'node 3' → Node details, diagnostics, link health\n\nNavigate time:\n- 'last night 2am' → timeline jumps there\n- 'yesterday kitchen' → filters timeline to kitchen events yesterday\n- 'this morning' → jumps to first detection today\n\nExecute commands:\n- 'update all nodes', 're-baseline kitchen', 'add node', 'arm security', 'disarm security'\n- 'dark mode'/'light mode', 'export config', 'restart node kitchen-north'\n\nGet help:\n- 'help fall detection', 'why false positive', 'troubleshoot kitchen'\n\nBehavior:\n- Fuzzy matching: 'flr pln' matches 'Floor Plan settings'\n- Recently used commands appear first\n- Results show keyboard shortcut hints where applicable\n- Escape closes, Enter executes top result\n- Works in expert mode only\n\n## Implementation\nFrontend-only component\nCommand registry maps keywords to actions\nSearch runs against: zone names, person names, node names, setting names, help topics\n\n## Acceptance\n- Ctrl+K/Cmd+K opens command palette\n- Search finds zones, people, nodes, settings, help topics\n- Commands execute correctly\n- Time navigation jumps to correct moments\n- Fuzzy matching works\n- Escape closes palette","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.571696739Z","updated_at":"2026-05-06T08:49:45.284413081Z","closed_at":"2026-05-06T08:49:45.284413081Z","close_reason":"Component 34: Command Palette fully implemented and tested.\n\n- Ctrl+K/Cmd+K opens universal search and command interface\n- Fuzzy matching across zones, people, nodes, settings, help topics\n- Time navigation: @3am, @-30min, @yesterday 11pm, @this morning, @last night\n- Commands execute: arm/disarm security, dark/light mode, export config, re-baseline, add node, etc.\n- Recently used commands appear first (up to 5)\n- Escape closes, Enter executes top result\n- Works in expert mode only (excluded from simple/ambient modes)\n- All 88 tests pass","source_repo":".","compaction_level":0} -{"id":"bf-4qwmy","title":"Simulator: GDOP overlay + shopping-list output (coverage gaps, recommended node count + positions)","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.874114137Z","updated_at":"2026-05-06T03:18:31.808932181Z","closed_at":"2026-05-06T03:18:31.808932181Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} -{"id":"bf-4truh","title":"Comprehensive notification system tests (open bead spaxel-40tl expansion)","description":"Open bead spaxel-40tl 'Write comprehensive tests for notification system' is open. The notify package (internal/notify/) has ntfy.go, pushover.go, webhook.go but tests are missing or incomplete. Needs tests covering: batching logic (30s dedup window), quiet hours gate (suppress non-critical during quiet window), morning digest aggregation, delivery retry logic, channel enable/disable, test-notification endpoint, and notification history API. The existing service_enhanced.go has complex batching logic that needs coverage.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:26:14.365679205Z","updated_at":"2026-05-02T18:26:51.067231767Z","closed_at":"2026-05-02T18:26:51.067231767Z","close_reason":"Duplicate of existing open bead spaxel-40tl which already comprehensively tracks notification system tests","source_repo":".","compaction_level":0} -{"id":"bf-55sg5","title":"Mobile-responsive expert mode","description":"## Goal\nMake expert mode fully functional on mobile devices with touch gestures.\n\n## Scope\n- Touch orbit/pan/zoom: single-finger rotate, two-finger pan, pinch to zoom (already supported by Three.js OrbitControls)\n- Hamburger menu for panels: collapsible sidebar for fleet status, settings, zones, triggers\n- Responsive layout: panels slide in from bottom on mobile, from right on desktop\n- Touch-optimized buttons: minimum 44×44px tap targets\n- No hover-dependent UI: all interactions work with tap\n- Mobile-specific shortcuts: long-press for context menu (replaces right-click)\n\n## Location\ndashboard/static/js/mobile.js (new module)\ndashboard/static/css/mobile.css (new stylesheet)\n\n## Acceptance\n- Three.js scene responds to touch gestures (orbit, pan, zoom)\n- Hamburger menu opens panel navigation\n- Panels slide in from bottom on mobile\n- All buttons are touch-friendly (≥44px)\n- No features require hover\n- Long-press context menu works on mobile","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.813640292Z","updated_at":"2026-05-06T11:04:43.020442678Z","closed_at":"2026-05-06T11:04:43.020442678Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-59me3","title":"GET /api/status and GET /api/occupancy endpoints","description":"Plan's REST API spec defines: (1) GET /api/status returning {version, nodes, blobs, uptime_s, detection_quality} and (2) GET /api/occupancy returning {zones:{:{count, people:[]}}}. Neither endpoint is registered in main.go. /api/blobs exists. These are simple read-only endpoints that dashboard and HA users would expect for quick system checks and occupancy queries without WebSocket.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:26.769707158Z","updated_at":"2026-05-05T17:40:16.692260524Z","closed_at":"2026-05-05T17:40:16.692260524Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-5fo3h","title":"Node disable/enable API endpoints","description":"Plan's REST API spec defines POST /api/nodes/:mac/disable (sets role to IDLE) and POST /api/nodes/:mac/enable (restores prior role). The fleet handler (internal/fleet/handler.go) has identify, reboot, OTA, position, role endpoints but no dedicated disable/enable. The quick-actions.js context menu exposes 'Disable / Enable' for nodes but there's no corresponding backend route.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:37.488896455Z","updated_at":"2026-05-05T18:13:57.459444611Z","closed_at":"2026-05-05T18:13:57.459444611Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-5o576","title":"Fuzz tests for binary frame parser and JSON protocol","description":"Open bead bf-3d55l tracks this but has been sitting open with no implementation started. The ingestion frame parser (internal/ingestion/frame.go) and JSON message parser (internal/ingestion/message.go) parse untrusted input from ESP32 nodes. Need Go fuzz tests (testing.F) in frame_fuzz_test.go and message_fuzz_test.go covering: malformed header lengths, n_sub overflow, invalid channel values, truncated payloads, invalid JSON type discriminators, and extra fields.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:25:51.799073946Z","updated_at":"2026-05-02T18:26:32.910185523Z","closed_at":"2026-05-02T18:26:32.910185523Z","close_reason":"Duplicate of existing open bead bf-3d55l which already tracks fuzz tests for binary frame parser and JSON protocol","source_repo":".","compaction_level":0} -{"id":"bf-5rulx","title":"Simulator: synthetic walkers + CSI generation (user paths, random walk, propagation model integration)","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.874026391Z","updated_at":"2026-05-06T02:45:17.053495568Z","closed_at":"2026-05-06T02:45:17.053495568Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} -{"id":"bf-5txbb","title":"Fleet status page","description":"## Goal\nFull table view of all registered nodes with all metrics, bulk actions, camera fly-to on click.\n\n## Scope\nTable columns:\n- Name: user-assigned friendly name\n- MAC: hardware address\n- Role: TX/RX/TX_RX — editable dropdown\n- Position: (x, y, z) — click to highlight node in 3D view and fly camera to it\n- Firmware: version string + 'Update available' badge\n- RSSI: last reported WiFi signal strength\n- Status: ONLINE/STALE/OFFLINE with colored indicator\n- Uptime: time since last boot\n- Actions: Restart, Update, Remove, Identify (blink LED)\n\nGlobal actions:\n- Update All (rolling OTA)\n- Re-baseline All\n- Export Config\n- Import Config\n\n## Location\ndashboard/static/js/fleet.js (new module, extract from existing code)\ninternal/api/fleet.go (already exists)\n\n## Acceptance\n- Table shows all registered nodes\n- Click position → camera flies to node in 3D view\n- Role dropdown changes node role\n- Actions execute correctly\n- Bulk actions work on all nodes\n- Export/import config works","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.834674580Z","updated_at":"2026-05-06T11:48:29.548615767Z","closed_at":"2026-05-06T11:48:29.548615767Z","close_reason":"Implemented fleet status page with full table view and bulk actions:\n- Added dashboard/static/js/fleet.js: core fleet management module with API functions\n- Added dashboard/static/css/fleet-page.css: complete styling for fleet table\n- Table columns: Name, MAC, Status, Firmware, Uptime, Position, Role, Health, Packet Rate, Temperature, Actions\n- Bulk actions: Update All (rolling OTA), Re-baseline All, Export Config, Import Config\n- Camera fly-to on position click\n- Node actions: identify (LED blink), update firmware, remove, re-assign role\n- CSV export for reporting","source_repo":".","compaction_level":0} -{"id":"bf-5vhya","title":"CI: pipeline timing benchmark gate","description":"## Goal\nAdd a benchmark that enforces the fusion loop timing budget as a CI quality gate, per plan §Quality Gates / Definition of Done (item 9).\n\n## What to build\n\nFile: internal/localizer/fusion/timing_budget_test.go\n\nRun the full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update) against synthetic CSI data from spaxel-sim output.\n\nAssert:\n- Median fusion iteration < 15 ms over 600 iterations (60 seconds at 10 Hz)\n- P99 < 40 ms (hard limit)\n\n## CI integration\nAdd to Argo Workflows CI step after go test ./...:\n go test -bench=BenchmarkFusionLoop -benchtime=60s -count=1 ./internal/localizer/fusion/ | tee /tmp/bench.txt\n # fail if median exceeds 15ms threshold\n\n## Acceptance\n- Benchmark runs in the Argo CI workflow\n- Workflow fails if median latency exceeds 15 ms on the CI runner (allowance: 2x for slower hardware → 30 ms CI threshold, 15 ms production target)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:00.487025943Z","updated_at":"2026-05-04T14:25:01.352506963Z","closed_at":"2026-05-04T14:25:01.352506963Z","close_reason":"Implementation already complete in commit 7afbdc9. The timing budget benchmark:\n\n1. File: mothership/internal/localizer/fusion/timing_budget_test.go\n - Runs full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update)\n - Uses synthetic CSI data simulating 4 nodes with 2 walkers\n - Runs 600 iterations (60 seconds at 10 Hz)\n\n2. Timing constraints enforced:\n - Median fusion iteration: 2.6ms (well below 15ms production target and 30ms CI threshold)\n - P99: ~10ms (well below 40ms hard limit)\n\n3. CI integration: .github/workflows/benchmark-ci.yml\n - Benchmark runs on every push/PR to main\n - Workflow fails if median exceeds 30ms (CI threshold)\n - Workflow fails if P99 exceeds 40ms (hard limit)\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0} -{"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":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.410156795Z","updated_at":"2026-05-06T05:05:27.767847993Z","closed_at":"2026-05-06T05:05:27.767847993Z","close_reason":"Fixed Fresnel zone debug overlay mouse interaction bugs:\n- Raycast against both wireframe and fill meshes for reliable detection\n- Wireframe is always present even when fill has 0 opacity (zones 2+)\n- Sort intersections by distance to get the closest intersection\n- Fixes hover detection not working properly for multi-zone Fresnel ellipsoids","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":"in_progress","priority":3,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:26:21.696828674Z","updated_at":"2026-05-06T13:53:20.089856369Z","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} -{"id":"bf-qonqo","title":"GET /api/zones/:id/history occupancy history endpoint","description":"Plan's REST API spec defines GET /api/zones/:id/history?period=24h returning [{timestamp, count, people:[]}] in hourly buckets. The zones CRUD exists (internal/api/zones.go) but the history sub-endpoint is not implemented. The anomaly/pattern system stores per-zone per-hour data in anomaly_patterns, and zone occupancy is tracked in memory and SQLite. Needed for the 'Zone history' quick action and the occupancy chart in the dashboard sidebar.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:26:02.249193078Z","updated_at":"2026-05-05T21:44:59.734545703Z","closed_at":"2026-05-05T21:44:59.734545703Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"bf-usafo","title":"Morning briefing (Component 35)","description":"## Goal\nWhen user first opens dashboard each day, brief warm summary appears.\n\n## Content (generated from existing data):\n- Sleep summary (if available): 'You slept 7h 39m — 12 minutes more than your average. Breathing was regular.'\n- Who is home: 'Bob left at 8:15am. The house has been empty since 8:22am.'\n- Overnight anomalies: 'Last night: One unusual event at 2:34am — motion in kitchen for 30 seconds. No BLE match, low-confidence blob. Likely environmental.'\n- System health: 'System health: Excellent (94%). All 6 nodes online. Accuracy improved 2% this week thanks to your 8 corrections.'\n- Today's forecast: 'Based on your Wednesday pattern, you usually return around 5:45pm. Security mode will auto-activate when you leave.'\n\nDisplay:\n- Expert mode: card overlay on first dashboard open, dismissible with tap or 'Got it' button. Slides away after 10s if not interacted\n- Simple mode: morning card is first card in layout, stays visible until dismissed\n- Ambient mode: text fades in over ambient display when first person detected in morning, stays for 30s\n\nDelivery channels:\n- Dashboard (default)\n- Push notification at configured time (e.g., 7am)\n- Webhook to Slack/Discord\n\n## Implementation\nGo function GenerateBriefing(date string, person string) string\nAssembled in priority order: critical alerts → sleep → who's home → anomalies → system health → predictions → learning progress\nStored as daily record in briefings table\n\n## Acceptance\n- Briefing accurately summarizes overnight activity\n- Shows sleep report when available\n- Lists overnight anomalies with context\n- Shows system health\n- Shows today's predictions\n- Dismissible and non-intrusive","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:11.592787579Z","updated_at":"2026-05-06T10:09:43.916190927Z","closed_at":"2026-05-06T10:09:43.916190927Z","close_reason":"Implemented Component 35: Morning Briefing\n\n- Added briefing dashboard adapter wiring to hub for morning briefing push\n- Morning briefing provides warm summary when user first opens dashboard each day\n- Content includes: sleep summary (if available), who is home, overnight anomalies, system health, predictions, learning progress\n- Briefing generated automatically via GenerateBriefing(date, person) function\n- Stored as daily record in briefings table\n- Delivery channels: dashboard (card overlay on first open), push notification at configured time, webhook to Slack/Discord\n- Display modes: expert mode (dismissible card overlay), simple mode (first card), ambient mode (fade-in text)\n- Briefing scheduler generates daily briefings at configured time and sends push notifications\n- API endpoints: GET/POST /api/briefing/* for managing briefings\n- All tests passing (go test ./internal/briefing/...)\n","source_repo":".","compaction_level":0} -{"id":"bf-w15bj","title":"CI: golangci-lint static analysis gate","description":"## Goal\nAdd golangci-lint to the Argo CI workflow as a hard quality gate, per plan §Quality Gates / Definition of Done (item 3).\n\n## Configuration\nFile: .golangci.yml at repo root\n\nEnabled linters (minimum set):\n- errcheck: all errors must be handled or explicitly discarded with _\n- staticcheck: includes S-series (simplifications) and SA-series (bugs)\n- gosimple: simplification suggestions (SA-series overlap)\n- govet: same as go vet but integrated\n- ineffassign: catch dead assignments\n- unused: catch unused exported identifiers\n\nDisabled (too noisy for this codebase):\n- gocyclo, funlen, wsl (style preferences, not correctness)\n\n## CI integration\nAdd to the spaxel-build Argo WorkflowTemplate as a parallel step alongside go test:\n golangci-lint run --timeout 5m ./...\n\n## Acceptance\n- golangci-lint run passes on the current codebase (fix any pre-existing findings before adding the gate)\n- Argo CI fails on new lint violations\n- .golangci.yml committed to repo root","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:09.633464353Z","updated_at":"2026-05-05T06:49:00.297475830Z","closed_at":"2026-05-05T06:49:00.297475830Z","close_reason":"Completed","source_repo":".","compaction_level":0} -{"id":"spaxel-05a","title":"Implement calibration GET endpoint","description":"## Task\nImplement GET /api/floorplan/calibrate endpoint.\n\n## Specification\n- Return current calibration from SQLite\n- Return 404 if no calibration exists\n\n## Acceptance\n- Returns calibration data when present\n- Returns 404 when no calibration exists","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T17:55:53.178762085Z","created_by":"coding","updated_at":"2026-04-07T18:58:38.551564957Z","closed_at":"2026-04-07T18:58:38.551463596Z","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-klk"]} -{"id":"spaxel-0fm8","title":"Add quiet hours gate tests","description":"Write tests for quiet hours gate: LOW at 23:00 with 22:00-07:00 quiet hours -> queued, URGENT at 23:00 -> delivered. Acceptance Criteria: Quiet hours tests pass (queueing, bypass).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-11T08:15:07.990827798Z","created_by":"coding","updated_at":"2026-05-04T06:32:11.422038645Z","closed_at":"2026-05-04T06:32:11.422038645Z","close_reason":"Quiet hours gate tests already exist and pass:\n- TestQuietHoursGate_LowAt23pmQueued: LOW at 23:00 with 22:00-07:00 quiet hours → queued\n- TestQuietHoursGate_UrgentAt23pmDelivered: URGENT at 23:00 → delivered\nAll acceptance criteria verified.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} -{"id":"spaxel-0ii","title":"Implement Zones CRUD REST endpoints","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Include OpenAPI-style godoc comments. Zone changes must reflect in live 3D view within one WebSocket cycle.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-07T13:56:27.275139529Z","created_by":"coding","updated_at":"2026-04-07T19:01:48.974563569Z","closed_at":"2026-04-07T19:01:48.974408083Z","close_reason":"Zones CRUD REST endpoints already fully implemented: GET/POST /api/zones, PUT/DELETE /api/zones/{id}, GET /api/zones/{id}/history, plus portals CRUD. OpenAPI godoc comments, WebSocket broadcasting for live 3D view, 31 table-driven tests. go vet and go test 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","mitosis-child","mitosis-depth:1","parent-spaxel-21n"],"dependencies":[{"issue_id":"spaxel-0ii","depends_on_id":"spaxel-3rd","type":"blocks","created_at":"2026-04-07T17:01:33.629176640Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-0ii","depends_on_id":"spaxel-5lo","type":"blocks","created_at":"2026-04-07T17:01:33.542274773Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-0j0k","title":"Implement responsive canvas resize and orientation handling","description":"Handle canvas resize on window resize and orientation change events, including iOS Safari visual viewport quirks and bottom navigation bar spacing.\n\n**Files:** dashboard/js/app.js (resize/orientationchange listeners), dashboard/css/expert.css (canvas height calculation)\n\n**Acceptance Criteria:**\n- Canvas resizes correctly on window resize event\n- Canvas resizes correctly on orientationchange event\n- renderer.setSize() called with updated dimensions\n- camera.aspect updated and camera.updateProjectionMatrix() called\n- Uses visualViewport.width/height on iOS Safari (fallback to window.innerWidth/Height)\n- Canvas height uses calc(100vh - 56px) when simple mode nav is visible","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T06:26:50.081049506Z","created_by":"coding","updated_at":"2026-04-11T06:51:20.327121642Z","closed_at":"2026-04-11T06:51:20.327061414Z","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-kth"]} -{"id":"spaxel-0n8r","title":"Adopt single design system (Radix dark tokens) across dashboard","description":"## Goal\n\nAll dashboard pages (`index.html`, `ambient.html`, `fleet.html`, `simple.html`, `integrations.html`) share one design system per plan.md §8e.\n\n## Scope\n\nCreate `dashboard/css/tokens.css` with CSS custom properties for:\n\n- **Colors:** Radix `slate` scale (12 steps) as `--slate-1` … `--slate-12`. Accent: Radix `blue` scale. Semantic: `--ok` (green-9), `--warn` (amber-9), `--alert` (red-9).\n- **Typography:** `--font-body`, `--font-mono`. Sizes: `--text-xs` (12), `--text-sm` (14), `--text-base` (16), `--text-lg` (20), `--text-2xl` (28).\n- **Spacing:** `--space-1` (4px) through `--space-16` (64px) on the canonical scale.\n- **Radius:** `--radius-control` (6px), `--radius-card` (10px), `--radius-modal` (16px).\n\nThen refactor each existing CSS file to use these tokens instead of hard-coded values. Delete any light-theme rules; dashboard is dark-only.\n\n`integrations.html` is the reference — its current look already approximates the target. Use it as the acceptance bar for other pages.\n\n## Verification\n\n- `grep -r \"#[0-9a-fA-F]\\{3,6\\}\" dashboard/css/` returns only uses inside `tokens.css`.\n- Playwright screenshots of all five pages show one consistent theme.\n- axe-core reports zero WCAG AA contrast violations.\n\nTraceability: plan.md §8e.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"feature","assignee":"quebec","owner":"","created_at":"2026-04-24T11:13:23.515071217Z","created_by":"coding","updated_at":"2026-04-24T20:48:18.439563795Z","closed_at":"2026-04-24T20:48:18.439456031Z","close_reason":"Verified: design token migration already complete. All 25 non-token CSS files use var(--token-name) exclusively. Zero hard-coded hex/rgba outside tokens.css. Zero light-theme rules. tokens.css contains full Radix dark scale (slate 1-12, blue 1-12, semantic colors, typography, spacing, radius). Prior commits 1e8876d and 07abc03 completed this work.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:50"]} -{"id":"spaxel-0w4","title":"Fleet status page","description":"## Background\n\nThe 3D scene is great for spatial context but poor for bulk fleet management tasks. With 6+ nodes, finding a specific node in the 3D view, checking its firmware version, and triggering an update involves hunting through the scene. The fleet status page provides a flat table view of all nodes with their key metrics and inline actions — the same information you would find in a server management panel, adapted for ESP32 nodes. It complements the 3D view rather than replacing it.\n\n## Fleet Status Table\n\nNew dashboard route: /fleet\n\nThe page layout:\n- Page header: \"Fleet Status\" title, total node count, online count, \"Update All\" button, \"Download report\" button\n- Filter and sort bar (below header)\n- Fleet table (main content)\n\nTable columns:\n1. Checkbox (for multi-select)\n2. Label (editable inline on double-click)\n3. MAC address (truncated, full on hover tooltip)\n4. Status: coloured dot + text. \"Online\" (green) / \"Offline\" (red) / \"Updating\" (yellow spinner)\n5. Firmware version: current version string. If a newer version is in the firmware manifest: version displayed in amber with an \"→ {new_version}\" indicator and an \"Update\" action badge.\n6. Uptime: formatted as \"3d 4h 12m\". Only valid when online.\n7. Current role: TX / RX / TX-RX / Passive. Small badge.\n8. Signal health: composite health score for this node's links as a small colour bar (green → red)\n9. Packet rate: \"{actual} / {configured} Hz\" as a fraction. Colour-coded: > 90% = green, 70-90% = amber, < 70% = red.\n10. Temperature: from health message. Shown as \"{N}°C\" or \"--\" if not reported. Alert colour if > 75°C.\n11. Actions column: [Locate] [OTA] [...more] buttons\n\nThe table rows are clickable (full row click = fly 3D camera to that node's position). Only clicking action buttons or the checkbox should not trigger the row fly-to.\n\n## Inline Label Edit\n\nDouble-clicking the label cell makes it inline-editable:\n- Input field replaces the text\n- Enter to confirm, Escape to cancel\n- Blur (click outside) to confirm\n- On confirm: PATCH /api/nodes/{mac}/label with the new label. Update the display.\n- Validation: max 32 characters, no control characters.\n\n## Action Buttons\n\n\"Locate\" (flash LED): sends a downstream command {\"type\":\"identify\"} to the node via WebSocket. The node flashes its onboard LED rapidly for 5 seconds. The button shows a spinner while the command is in-flight, then a brief green checkmark.\n\n\"OTA\" (firmware update): available only if the node's firmware version != latest in the manifest. Clicking shows a confirmation tooltip: \"Update Node [label] from v{current} to v{latest}? [Confirm] [Cancel]\". On confirm: POST /api/nodes/{mac}/ota. The node's row shows \"Updating\" status and a progress bar (populated from ota_status WebSocket messages for this node).\n\n\"More actions\" (... button): dropdown with: \"Re-assign role\", \"View health history\", \"View event history\", \"Remove from fleet\". Each with an appropriate icon.\n\n## Bulk Actions\n\nCheckbox column allows multi-selecting rows. When any row is selected, a bulk-actions bar slides in above the table:\n- \"Update {N} selected to latest firmware\" — confirms and triggers OTA for all selected nodes in sequence (with 30s stagger)\n- \"Re-assign roles\" — opens the role optimiser with the selected nodes included\n- \"Remove {N} from fleet\" — confirmation required: lists the nodes to be removed\n\nDeselect all: \"Clear selection\" button in the bulk actions bar, or uncheck all checkboxes.\n\n## Camera Fly-To\n\nClicking a table row (non-action click) triggers a smooth camera fly-to the node's position in the expert mode 3D view. The fleet page and expert mode are on different routes, so this requires:\n1. Store the target node MAC in localStorage or URL parameter (\"?highlight={mac}\")\n2. Redirect to the expert mode route / (or open expert mode in a second tab)\n3. On load, if ?highlight={mac} parameter is present, fly camera to that node's position\n\nAlternatively: if the fleet page is opened alongside the expert mode in a split-pane layout (future enhancement), coordinate via a shared state store. For Phase 9, the redirect approach is sufficient.\n\n## Sorting and Filtering\n\nColumn header click: sort by that column. First click = ascending, second = descending. Sort state shown with a small arrow indicator.\n\nFilter row (below column headers, toggle-able with a \"Filter\" button):\n- Label / MAC: text input, filters rows containing the substring\n- Status: dropdown \"All / Online / Offline\"\n- Firmware: dropdown \"All / Outdated only\"\n- Role: multi-select dropdown \"All / TX / RX / TX-RX / Passive\"\n\nActive filters: shown as chips above the table with individual dismiss buttons. \"Clear all filters\" link.\n\n## Download Report\n\n\"Download report\" button: exports the current fleet table (including all filters) as a CSV file. Columns: MAC, label, status, firmware_version, uptime_s, role, health_score, packet_rate_hz, temperature_c, last_seen.\n\nImplemented as a client-side CSV generation from the current table data (no API call needed if data is cached in the dashboard state). Use Blob + URL.createObjectURL for download.\n\n## REST API\n\nGET /api/fleet: returns all provisioned nodes with full details (same as GET /api/nodes but with more fields: uptime, firmware_version, temperature, health_score, packet_rate).\nPATCH /api/nodes/{mac}/label: update label\nPOST /api/nodes/{mac}/locate: send identify command\nPOST /api/nodes/{mac}/role: assign new role\nDELETE /api/nodes/{mac}: remove from fleet (disconnects and archives)\n\n## Files to Create or Modify\n\n- dashboard/fleet.html: fleet page HTML shell\n- dashboard/js/fleet.js: table rendering, sorting, filtering, bulk actions, inline edit\n- dashboard/css/fleet.css: fleet page styles\n- mothership/internal/dashboard/routes.go: fleet-specific API routes, /fleet HTML route\n\n## Tests\n\n- Test fleet table renders correctly with mock data: 4 nodes, verify all columns populated\n- Test inline label edit: double-click cell, type new label, Enter -> PATCH API called with correct body\n- Test bulk selection: check 3 nodes, verify bulk actions bar appears with correct count\n- Test bulk OTA triggers OTA for all 3 selected nodes\n- Test sorting: click \"Firmware version\" header -> rows sorted ascending by version string\n- Test filter: enter \"living\" in label filter -> only rows matching \"living\" visible\n- Test camera fly-to: clicking a row stores MAC in localStorage and redirects to expert mode with ?highlight parameter\n- Test CSV download: verify blob is created with correct headers and values\n\n## Acceptance Criteria\n\n- Fleet page loads with all nodes and their current metrics\n- Inline label edit saves correctly to the API and updates the display\n- Bulk OTA fires for all selected nodes with correct stagger\n- Sorting and filtering work correctly for all columns\n- Camera fly-to positions the 3D view correctly on the selected node after redirect\n- CSV download contains correct headers and all fleet data\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"romeo","owner":"","created_at":"2026-03-28T02:06:06.562532476Z","created_by":"coding","updated_at":"2026-04-25T12:49:56.798295553Z","closed_at":"2026-04-25T12:49:56.798220896Z","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:33"],"dependencies":[{"issue_id":"spaxel-0w4","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:14.955755499Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-16e","title":"Captive portal WiFi recovery","description":"## Background\n\nIf a node loses WiFi connectivity — because a router was replaced, credentials changed, or the node was moved out of range — it currently has no recovery path without physical intervention. The firmware already implements a captive portal AP fallback in firmware/main/wifi.c: after 10 consecutive WiFi connection failures with exponential backoff (up to 30s between attempts), the firmware transitions to AP mode with SSID \"spaxel-{last4mac}\". However, the captive portal HTTP config page served in this mode is currently a stub and needs to be completed. This bead finishes that implementation and adds the mothership-side offline detection and dashboard alert.\n\n## What Already Exists (Phase 1)\n\nfirmware/main/wifi.c implements the WIFI_STATE_CAPTIVE_PORTAL state transition after failure threshold. The firmware starts a SoftAP with SSID \"spaxel-{last4mac}\" and password-free (open AP). A stub HTTP server exists but serves only a 200 OK response with no content. The DNS server for captive portal hijacking is not yet implemented.\n\n## Captive Portal HTTP Server (Firmware)\n\nUse ESP-IDF's esp_http_server component to serve a config page at http://192.168.4.1/ when in AP mode.\n\nThe config page (served as inline HTML, no external resources — it must work fully offline):\n- Shows current provisioned SSID (from NVS, read-only display)\n- Form: new SSID input, password input, optional mothership host/port\n- Submit button: \"Save and Reconnect\"\n- On form submission: firmware writes new credentials to NVS (wifi_ssid, wifi_pass, optionally mothership_host/port) and calls esp_restart()\n- Minimal HTML with inline CSS: max 4KB so it fits in IRAM. No JavaScript needed.\n- Works on iOS Safari (no JS requirement), Android Chrome, desktop browsers\n\nThe HTTP server must handle the iOS and Android captive portal probe paths:\n- GET /hotspot-detect.html (iOS)\n- GET /generate_204 (Android)\n- GET /ncsi.txt (Windows)\nRespond to all of these with a 302 redirect to http://192.168.4.1/ to trigger the OS captive portal popup.\n\n## DNS Hijacking for Captive Portal Detection\n\nModern mobile OSes detect captive portals by making a DNS lookup for a known hostname (connectivitycheck.android.com, captive.apple.com, etc.) and checking if the response is correct. To trigger the OS captive portal popup automatically (so the user does not need to manually navigate to 192.168.4.1):\n\nImplement a minimal DNS server using lwIP's UDP API. The DNS server binds to UDP port 53 on the SoftAP interface (192.168.4.1). It responds to ALL DNS queries with an A record pointing to 192.168.4.1, regardless of the queried hostname. This causes the OS to detect the captive portal and show the popup.\n\nESP-IDF does not provide a ready-made DNS hijacking component — implement using esp_event_loop and lwIP udp_new()/udp_bind()/udp_recv() APIs. Keep the implementation minimal: parse enough of the DNS query to extract the transaction ID and question name, then build a minimal A record response.\n\n## Mothership Offline Detection and Dashboard Alert\n\nThe mothership detects node disconnection via heartbeat timeout. When a node's WebSocket closes (or health messages stop for > 30s), the mothership:\n1. Sets node status to OFFLINE in the node registry\n2. Broadcasts a node_offline WebSocket message to dashboard: {\"type\":\"node_offline\",\"mac\":\"aa:bb:cc:dd:ee:ff\",\"label\":\"Living Room\",\"last_seen\":\"2026-03-27T14:23:00Z\",\"likely_cause\":\"wifi_loss\"}\n\nThe dashboard shows an offline alert card:\n- \"Node [Living Room] went offline at 14:23. It may need WiFi reconfiguration.\"\n- Shows the captive portal AP SSID to connect to: \"Connect to Wi-Fi network 'spaxel-ff01' to reconfigure\"\n- Troubleshooting steps: 1) Check power LED, 2) Check WiFi router is online, 3) Connect to captive portal if LED is blinking\n\nThe likely_cause field is set by the mothership based on the last health message before disconnect (e.g. low WiFi RSSI in last health message -> \"wifi_range\", no recent health messages at all -> \"unknown\").\n\n## Reconnection and Portal Exit\n\nWhen the user submits new credentials in the captive portal and the node reboots:\n1. The SoftAP goes down (existing connections to \"spaxel-ff01\" are dropped)\n2. The node attempts WiFi connection with new credentials\n3. On success, connects to mothership — mothership sets status to ONLINE and broadcasts node_online event\n4. Dashboard shows reconnection notification: \"Node [Living Room] reconnected successfully\"\n\nIf the new credentials also fail (e.g. user mistyped), the node re-enters captive portal mode after another 10 failures. The portal should display an error on re-entry if the previous attempt failed.\n\n## Implementation Files\n\n- firmware/main/wifi.c: complete captive portal HTTP server, add DNS hijacking task\n- firmware/main/captive_portal.c (new): DNS hijacking task + HTTP handler functions\n- mothership/internal/ingestion/server.go: heartbeat timeout detection, node_offline event\n- mothership/internal/fleet/manager.go: node status tracking (ONLINE/OFFLINE/CAPTIVE_PORTAL)\n- dashboard/js/app.js: node_offline event handler, offline alert card rendering\n\n## Testing Challenges\n\nThis feature is difficult to unit test due to hardware dependency. Recommended approaches:\n1. Firmware: Integration test in QEMU (esp32s3 target) — simulate WiFi failures by mocking the WiFi event loop, verify state machine transitions to CAPTIVE_PORTAL after failure threshold\n2. DNS server: Unit test the DNS response builder function with a fixed query buffer and verify the response parses correctly\n3. HTTP config page: Unit test the form handler that writes to NVS (mock the NVS API)\n4. Mothership: Unit test heartbeat timeout detection with a fake time source, verify node_offline event is emitted\n\n## Acceptance Criteria\n\n- After WiFi failure (10 consecutive failures), node enters AP mode with SSID \"spaxel-{last4mac}\" within 5 minutes of initial loss\n- Mobile device connecting to the captive portal AP automatically sees the OS captive portal popup (tested on iOS 16+ and Android 12+)\n- Config page is served and functional without JavaScript\n- Updating credentials causes node to reboot and reconnect to mothership within 60s\n- Dashboard shows offline node alert within 30s of disconnection\n- Reconnection notification appears in dashboard within 30s of node reconnecting\n- Captive portal DNS server responds correctly to all DNS queries with 192.168.4.1\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T01:38:15.579673840Z","created_by":"coding","updated_at":"2026-03-28T05:36:39.291892350Z","closed_at":"2026-03-28T05:36:39.291803146Z","close_reason":"Implemented: firmware/main/wifi.c (fb69190, 89 lines added) — captive portal AP mode (spaxel-XXXX SSID), esp_http_server config page at 192.168.4.1, DNS hijacking to trigger OS captive portal popup, NVS credential update on form submission","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-16e","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T03:29:13.901241816Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-16z3","title":"Add webhook delivery tests","description":"Write tests for webhook delivery: verifies JSON structure and base64 PNG field. Acceptance Criteria: Webhook delivery tests pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:08.130589490Z","created_by":"coding","updated_at":"2026-04-11T09:07:17.379388848Z","closed_at":"2026-04-11T09:07:17.379333570Z","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:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} -{"id":"spaxel-17u","title":"Phase 9: UX Polish & Accessibility","description":"Goal: Accessible to every household member. Power user efficiency.\n\nDeliverables:\n- Simple mode (card-based mobile-first UI, room occupancy cards, activity feed)\n- Ambient dashboard mode (/ambient for wall tablets, simplified top-down, auto-dim)\n- Spatial quick actions (right-click context menus on 3D elements, follow camera)\n- Command palette (Ctrl+K universal search/command, fuzzy matching)\n- Morning briefing (daily summary card, push notification option)\n- Guided troubleshooting (proactive contextual help, post-feedback explanations)\n- Mobile-responsive expert mode (touch orbit/pan/zoom)\n- Fleet status page (full table, bulk actions, camera fly-to)\n\nExit criteria: Non-technical user can check occupancy without training. Ambient mode runs 7+ days.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"phase","assignee":"golf","owner":"","created_at":"2026-03-27T01:55:55.188364609Z","created_by":"coding","updated_at":"2026-04-10T09:52:30.053325636Z","closed_at":"2026-04-10T09:52:30.053221884Z","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:2"],"dependencies":[{"issue_id":"spaxel-17u","depends_on_id":"spaxel-2tlm","type":"blocks","created_at":"2026-04-10T02:03:09.311198456Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-2x1w","type":"blocks","created_at":"2026-04-10T02:03:09.525865595Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-7b3g","type":"blocks","created_at":"2026-04-10T02:03:09.377986112Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-bsek","type":"blocks","created_at":"2026-04-10T02:03:09.442882165Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-duvd","type":"blocks","created_at":"2026-04-10T02:03:09.577884728Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-eelr","type":"blocks","created_at":"2026-04-10T02:03:09.654765280Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-jxru","type":"blocks","created_at":"2026-04-10T02:03:09.755196510Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T01:33:53.433780442Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-17u","depends_on_id":"spaxel-trsm","type":"blocks","created_at":"2026-04-10T02:03:09.707647462Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-19h","title":"fix: localization/spatial_weights.go unused variable linkID","description":"## Problem\n`internal/localization/spatial_weights.go:601` declares `linkID` but never uses it, causing build failure.\n\n## Fix\nFind line 601 in `mothership/internal/localization/spatial_weights.go` and either:\n- Remove the `linkID` variable assignment, or\n- Use the value (pass it somewhere meaningful), or\n- Replace with blank identifier: `linkID` → `_`\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/localization/\n```\nMust compile with no errors.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T22:29:51.085342479Z","created_by":"coding","updated_at":"2026-04-06T22:35:21.502259769Z","closed_at":"2026-04-06T22:35:21.502022402Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} -{"id":"spaxel-1l2j","title":"Implement GET /api/events/{id} detail endpoint","description":"Implement GET /api/events/{id} for single event detail. Acceptance Criteria: Returns 404 for non-existent event IDs; Returns full event details for valid IDs.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T18:14:45.476239078Z","created_by":"coding","updated_at":"2026-04-09T19:09:34.130476972Z","closed_at":"2026-04-09T19:09:34.130355809Z","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-yeh"]} -{"id":"spaxel-1xt","title":"events: REST API GET /api/events with FTS5 search and cursor pagination","description":"## Overview\nImplement the HTTP handler for the events REST API (part 3 of spaxel-2ap split).\n\n## Endpoint\n`GET /api/events`\n\nQuery params:\n- `limit` (default 50, max 500)\n- `before` (cursor: timestamp_ms as string — keyset pagination, NOT OFFSET)\n- `after` (ISO8601 datetime)\n- `type` (filter by event type)\n- `zone` (filter by zone name)\n- `person` (filter by person label)\n- `q` (FTS5 query — prefix matching, e.g. `q=motion*`)\n\nResponse:\n```json\n{\"events\": [...], \"cursor\": \"1744000000000\", \"has_more\": true, \"total_filtered\": 42}\n```\n\n## Implementation\n- Register at `/api/events` in the existing chi router (requires spaxel-6ha is wired, but can be added independently)\n- FTS5 query via: `SELECT e.* FROM fts_events ft JOIN events e ON e.id = ft.rowid WHERE fts_events MATCH ? ORDER BY e.timestamp_ms DESC LIMIT ?`\n- Keyset: add `AND timestamp_ms < ?` when `before` cursor is provided\n- total_filtered: COUNT from same query without LIMIT\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n# Run mothership and curl:\n# curl 'http://localhost:8080/api/events?limit=10&q=detection*'\n```\n\nRequires: spaxel-4u6 (schema must exist first)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-06T22:31:17.453879797Z","created_by":"coding","updated_at":"2026-04-07T17:27:48.091794931Z","closed_at":"2026-04-07T17:27:48.091693315Z","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"],"dependencies":[{"issue_id":"spaxel-1xt","depends_on_id":"spaxel-4u6","type":"blocks","created_at":"2026-04-06T22:31:24.454688305Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-1ydd","title":"Fix viz3d.js duplicate _renderer declaration","description":"## Problem\n\n`js/viz3d.js` declares `_renderer` twice:\n\n- Line 13: `let _scene, _camera, _controls, _clock, _renderer;`\n- Line 30: `let _renderer = null;`\n\nThis throws `SyntaxError: Identifier '_renderer' has already been declared` when the dashboard loads, killing the entire Viz3D IIFE. The cascade failure is what causes the home page's overlapping-panels visual chaos (Playwright review 2026-04-24).\n\n## Fix\n\nDelete line 30 of `js/viz3d.js`. Initialize `_renderer` in the existing declaration on line 13, or leave the declaration and assign later — whichever is consistent with how other private module state is handled in that file.\n\n## Verification\n\n1. Load the dashboard home page; open DevTools console.\n2. Confirm no `SyntaxError: Identifier '_renderer' has already been declared`.\n3. Confirm `Viz3D` global is defined (`typeof Viz3D === 'object'`).\n4. Run existing unit tests: `cd dashboard && npm test` (Jest suite covers viz3d).\n\n## Why this first\n\nThis is the single highest-leverage fix in the UI remediation queue — it unblocks several other visible defects listed in plan.md §8f.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"bug","assignee":"charlie","owner":"","created_at":"2026-04-24T11:13:01.111983612Z","created_by":"coding","updated_at":"2026-04-24T11:46:55.326179839Z","closed_at":"2026-04-24T11:46:55.326103741Z","close_reason":"Fixed duplicate _renderer declaration in dashboard/js/viz3d.js. Removed second 'let _renderer = null' on line 30 that conflicted with line 13. Commit c552ccd. Go tests pass, go vet clean.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:9"]} -{"id":"spaxel-21n","title":"Implement Zones and Portals REST endpoints","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Implement CRUD for portals: GET/POST /api/portals, PUT/DELETE /api/portals/{id}. Changes must reflect in live 3D view within one WebSocket cycle. Include OpenAPI-style godoc comments.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-06T15:31:10.270709535Z","created_by":"coding","updated_at":"2026-04-07T19:21:31.773484992Z","closed_at":"2026-04-07T19:21:31.773069063Z","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:1","mitosis-child","mitosis-depth:1","parent-spaxel-6ha"],"dependencies":[{"issue_id":"spaxel-21n","depends_on_id":"spaxel-0ii","type":"blocks","created_at":"2026-04-07T13:56:27.311077260Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-21n","depends_on_id":"spaxel-fi6","type":"blocks","created_at":"2026-04-07T13:56:27.361443212Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-26o","title":"Dashboard presence indicator","description":"## Background\n\nPhase 2 signal processing (phase sanitisation, baseline, motion detection) is complete in mothership/internal/signal/. The pipeline produces per-link MotionState (IsMotion bool, DeltaRMS float64, Confidence float64) via ProcessorManager.GetAllMotionStates(). This bead surfaces that data in the browser dashboard — the first human-visible output of the detection pipeline.\n\n## What to Implement\n\n1. Server-side: Add a periodic broadcast (every 500ms) of all link motion states as a JSON WebSocket message type 'presence_update'. Message schema: {type:'presence_update', links: {linkID: {is_motion: bool, delta_rms: float, confidence: float}}}. Wire into mothership/internal/dashboard/hub.go — Hub already has a Broadcast method.\n\n2. Frontend (dashboard/js/app.js): Add a 'Presence' panel distinct from the raw amplitude bar chart. Per-link rows showing: link ID (nodeMAC:peerMAC abbreviated), coloured circle indicator (green = clear, amber = motion, red = high-confidence motion), deltaRMS value. Click a link row to select it for the amplitude time series.\n\n3. Amplitude time series: Rolling 10s buffer of deltaRMS values per link. Render as a Canvas 2D line chart below the presence panel. X-axis: time (10s window), Y-axis: deltaRMS (0..0.1 typical range). Show threshold line at 0.02 (DefaultDeltaRMSThreshold).\n\n## Key Files\n\n- mothership/internal/signal/processor.go — GetAllMotionStates(), MotionState struct\n- mothership/internal/dashboard/hub.go — Hub.Broadcast(message interface{})\n- mothership/internal/dashboard/server.go — WebSocket handler, periodic broadcast setup\n- dashboard/js/app.js — existing UI code to extend\n\n## Acceptance Criteria\n\n- presence_update messages broadcast every 500ms with all active link states\n- Dashboard shows per-link coloured motion indicator updating in real-time\n- Amplitude time series shows last 10s of deltaRMS for selected link\n- Threshold line visible at 0.02\n- All existing tests pass (cargo check equivalent: go test ./...)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-03-28T03:29:50.484956631Z","created_by":"coding","updated_at":"2026-03-28T03:56:59.340530333Z","closed_at":"2026-03-28T03:56:59.340245276Z","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"]} -{"id":"spaxel-288","title":"Add NTP server to provisioning payload","description":"Update mothership provisioning system:\n- Read SPAXEL_NTP_SERVER env var (default: pool.ntp.org)\n- Embed ntp_server field in provisioning payload JSON\n- Support config downstream message field ntp_server to push updated server to nodes\n\nAcceptance: NTP server is configurable via provisioning payload and can be updated via downstream config messages.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-07T14:37:00.339721504Z","created_by":"coding","updated_at":"2026-04-07T17:39:12.764197464Z","closed_at":"2026-04-07T17:39:12.764097159Z","close_reason":"NTP server configuration is now fully implemented:\n\n1. SPAXEL_NTP_SERVER env var read with default 'pool.ntp.org'\n2. ntp_server field embedded in provisioning payload JSON\n3. ConfigMessage supports ntp_server field for downstream updates\n4. SendNTPServerToMAC() function available to push NTP server changes to nodes\n\nThe mothership now passes the configured NTP server to nodes during provisioning and can update it at runtime via config messages.","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-u7y"]} -{"id":"spaxel-28j7","title":"Add floor-plan renderer tests","description":"Write tests for floor-plan renderer that verify: renderer produces 300x300 PNG with correct dimensions, zone boundaries appear at correct pixel coordinates, colors are correct. Acceptance Criteria: Renderer tests pass (dimensions, coordinates, colors).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:07.870662320Z","created_by":"coding","updated_at":"2026-04-11T08:30:13.045511455Z","closed_at":"2026-04-11T08:30:13.045452030Z","close_reason":"Implemented comprehensive floor-plan renderer tests:\n- TestRendererDimensions: Verifies PNG output is 300x300 pixels (and custom sizes)\n- TestZoneBoundariesAtCorrectCoordinates: Verifies zone boundaries appear at correct pixel coordinates\n- TestZoneBoundaryEdges: Verifies zone edge detection by sampling pixels\n- TestPixelColors: Verifies background color, person blob colors, and brightness\n- All renderer tests pass\n\nValidates: 300x300 PNG dimensions, correct zone boundary coordinates, accurate colors","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} -{"id":"spaxel-28k","title":"Add event messages to WebSocket feed","description":"Add 'event' message type to /ws/dashboard for presence transitions, zone entries/exits, and portal crossings. Broadcast: { type: 'event', event: { id, ts, kind, zone, blob_id, person_name } }. Handle in app.js onmessage. Events appear within 1s of zone transition.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T14:18:27.377328251Z","created_by":"coding","updated_at":"2026-04-07T15:14:57.597424097Z","closed_at":"2026-04-07T15:14:57.597088117Z","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:23","mitosis-child","mitosis-depth:1","parent-spaxel-9eg"]} -{"id":"spaxel-2ap","title":"Activity timeline backend: events API, FTS5 search, cursor pagination","description":"## Overview\nBackend for the unified activity timeline — event ingestion, storage, full-text search, and REST API with cursor pagination. Feeds the timeline dashboard UI (spaxel-65k).\n\n## SQLite schema\n- events table: id INTEGER PK, type TEXT, timestamp_ms INTEGER, zone TEXT, person TEXT, blob_id TEXT, detail_json TEXT, severity TEXT\n- FTS5 virtual table fts_events: content='events', content_rowid='id' — indexed on type, zone, person, detail_json\n- events_archive: same schema, auto-migrated from events when timestamp_ms < now - 90 days (nightly at 02:00 local)\n- Indexes: idx_events_ts (timestamp_ms DESC), idx_events_type, idx_events_zone, idx_events_person\n\n## Event types to ingest\ndetection, zone_entry, zone_exit, portal_crossing, trigger_fired, fall_alert, anomaly, security_alert, node_online, node_offline, ota_update, baseline_changed, system, learning_milestone\n\n## REST API\n- GET /api/events — query params: limit (default 50, max 500), before (cursor), after (ISO8601), type, zone, person, q (FTS5 query)\n- Response: {events: [...], cursor: '', has_more: bool, total_filtered: int}\n- Keyset pagination using timestamp_ms as cursor (no OFFSET)\n- FTS5 query via fts_events MATCH q; fuzzy: prefix matching via q*\n\n## Event publishing\n- All event types published to dashboard WS feed as 'event' messages (requires spaxel-9eg)\n- Event bus in Go: internal publish/subscribe so any package can emit events without direct dependency on dashboard\n\n## Acceptance\n- 1000 events query <50ms with FTS5 search\n- Cursor pagination returns consistent results across pages\n- Archive job runs at 02:00 and moves events >90 days\n- FTS5 search matches partial words (prefix mode)\n- Requires: spaxel-6ha (REST API wiring), spaxel-9eg (WS event feed)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T13:02:26.845982082Z","created_by":"coding","updated_at":"2026-04-07T19:32:10.083144442Z","closed_at":"2026-04-07T19:32:10.082803006Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["blocked","deferred"],"dependencies":[{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-1xt","type":"blocks","created_at":"2026-04-06T22:31:24.411971684Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-4u6","type":"blocks","created_at":"2026-04-06T22:31:24.308501610Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-2ap","depends_on_id":"spaxel-6n9","type":"blocks","created_at":"2026-04-06T22:31:24.359488476Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-2ea","title":"Add alert messages to WebSocket feed","description":"Add 'alert' message type to /ws/dashboard for anomaly detections and security mode triggers. Broadcast: { type: 'alert', alert: { id, ts, severity, description, acknowledged } }. Handle in app.js onmessage.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T14:18:27.455727878Z","created_by":"coding","updated_at":"2026-04-07T11:04:25.716894375Z","closed_at":"2026-04-07T11:04:25.716743770Z","close_reason":"Alert message type already fully implemented: BroadcastAlert() in hub.go broadcasts {type:'alert', alert:{id,ts,severity,description,acknowledged}} to /ws/dashboard clients. Called from anomaly detection, security mode changes, and trigger-disabled alerts. Frontend handleAlertMessage() in app.js routes the alert type, shows toast notifications, logs to timeline, and triggers alert banner. Table-driven tests pass (4 cases). go vet clean.","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-9eg"]} -{"id":"spaxel-2tlm","title":"Implement simple mode UI","description":"Card-based mobile-first UI with room occupancy cards and activity feed. Non-technical users should be able to check occupancy without training.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-10T02:03:09.271050009Z","created_by":"coding","updated_at":"2026-04-10T02:08:15.912199673Z","closed_at":"2026-04-10T02:08:15.912102801Z","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-17u"]} -{"id":"spaxel-2wg","title":"BLE device registry and labelling","description":"## Background\n\nThe firmware scans BLE advertisements every 5 seconds and relays them to the mothership via the bidirectional protocol (spaxel-o4l, Phase 3). Each BLE relay message contains a list of {mac, name, rssi, manufacturer_data} tuples for all devices heard by that node in the last 5 seconds. Phase 6 turns this raw stream into a structured \"People and Devices\" registry where users can label their devices and associate them with named people. This is the identity layer that transforms anonymous CSI blobs into \"Alice\" and \"Bob\".\n\n## BLE Device Auto-Detection\n\nThe mothership can identify device types from manufacturer data embedded in BLE advertisement packets. The Bluetooth SIG assigns Company IDs to manufacturers; the first 2 bytes of manufacturer_data encode the company ID (little-endian).\n\nCompany IDs to detect:\n- 0x004C (Apple): likely iPhone, iPad, AirPods, or Apple Watch. Sub-type from manufacturer data length and flags.\n- 0x0006 (Microsoft): Windows devices\n- 0x0075 (Samsung): Samsung phones/tablets\n- 0x009E (Fitbit): Fitness trackers\n- 0x0157 (Garmin): GPS watches / fitness devices\n- 0x0059 (Nordic): Tile trackers (Nordic Semiconductor is used by many Tile-like devices)\n- 0x0499 (Ruuvi): Ruuvi temperature/humidity sensors\n- 0x00E0 (Google): Android devices (Nearby Share beacons)\nClassify all others as \"Unknown\". The device name field (if present in the advertisement) provides additional signal.\n\nWearable heuristic: RSSI typically -55 to -75 dBm across multiple nodes with relatively consistent signal (worn close to body). Static devices (speakers, tablets) show higher variance. Flags this heuristic as \"possibly wearable\" (not definitive).\n\n## BLERegistry\n\nNew package: mothership/internal/identity/ble.go\n\nBLERegistry struct: backed by SQLite table ble_devices.\n\nSQLite schema:\nCREATE TABLE ble_devices (\n mac TEXT PRIMARY KEY,\n name TEXT,\n manufacturer TEXT,\n device_type TEXT, -- apple_phone, apple_earbuds, fitbit, garmin, tile, samsung, unknown\n label TEXT, -- user-assigned label\n person_id TEXT, -- FK to people.id\n rssi_min INTEGER,\n rssi_max INTEGER,\n rssi_avg INTEGER,\n first_seen DATETIME,\n last_seen DATETIME,\n is_archived BOOLEAN DEFAULT FALSE,\n last_seen_node_mac TEXT\n);\n\nCREATE TABLE people (\n id TEXT PRIMARY KEY, -- uuid\n name TEXT NOT NULL,\n color TEXT, -- hex colour for dashboard rendering\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE person_devices (\n person_id TEXT,\n device_mac TEXT,\n PRIMARY KEY (person_id, device_mac)\n);\n\nBLERegistry methods:\n- ProcessRelayMessage(nodeMac string, devices []BLEDevice): upsert all devices, update last_seen, update RSSI stats\n- GetDevices(includeArchived bool) []BLEDeviceRecord\n- UpdateLabel(mac, label string) error\n- AssignToPerson(mac, personID string) error\n- CreatePerson(name, color string) (Person, error)\n- GetPeople() []Person\n- ArchiveStale(olderThan time.Duration): set is_archived=true for devices not seen for > olderThan\n\n## BLE MAC Randomisation Handling\n\nModern iPhones and Android phones randomise their BLE MAC address periodically (every 10-15 minutes for iPhones, similar for Android). This is a fundamental privacy feature. The implications for spaxel:\n\n1. The same physical phone appears as multiple different MAC addresses in the registry. The BLERegistry will create new entries for each rotated address.\n2. Long-term tracking of phones by MAC is unreliable. The registry will accumulate many entries for a single phone over time.\n3. Workarounds: (a) Apple uses Resolvable Private Addresses (RPA) that can be resolved with the Identity Resolving Key (IRK) — requires pairing, not available without user action. (b) Device name is sometimes consistent across rotations. (c) Wearable devices (Fitbit, Garmin, AirTag) typically do NOT rotate their MACs — they provide reliable long-term tracking.\n\nThe dashboard must clearly explain this limitation in the \"People and Devices\" panel:\n\"Your phone's Bluetooth address changes regularly for privacy reasons. For reliable person tracking, use a Fitbit, Garmin watch, or AirTag, which have stable addresses.\"\n\nGrouping heuristic: if two devices have the same manufacturer data prefix (first 6 bytes) and name, and were never seen simultaneously at high RSSI from the same node, they are likely the same device with a rotated MAC. Surface this as a \"possible duplicate\" suggestion in the UI: \"These may be the same device: [mac1] and [mac2]. Merge?\"\n\n## REST API\n\nGET /api/ble/devices: returns list of BLEDeviceRecord, optionally filtered by ?archived=true\nGET /api/ble/devices/{mac}: returns single device with full history\nPUT /api/ble/devices/{mac}: update label, device_type, or person assignment. Body: {\"label\":\"Alice's Phone\",\"device_type\":\"apple_phone\",\"person_id\":\"uuid-123\"}\nDELETE /api/ble/devices/{mac}: archive (not hard delete)\n\nGET /api/people: returns list of People with their associated devices\nPOST /api/people: create person. Body: {\"name\":\"Alice\",\"color\":\"#3b82f6\"}\nPUT /api/people/{id}: update name or color\nDELETE /api/people/{id}: soft-delete (retain historical data)\n\n## Dashboard Panel\n\n\"People and Devices\" sidebar panel showing:\n- People section: list of defined people with avatar (initials in circle with their color), device count, last seen time\n - Per person: click to expand, shows associated devices\n - \"Add person\" button opens inline form\n- All devices section (below people): list of devices not yet assigned to a person\n - Per device: device type icon (Apple logo, Fitbit icon, etc.), last seen node (abbreviated), last seen timestamp, RSSI bar\n - Inline label edit on double-click\n - Drag-and-drop to assign to a person card\n - Archive button (hides from active list, accessible via \"Show archived\" toggle)\n- Privacy notice: \"Phones may appear multiple times due to address rotation. Wearables and AirTags have stable addresses.\"\n\n## Tests\n\n- Test device auto-detection: Apple company ID 0x004C -> device_type \"apple_phone\", Fitbit 0x009E -> \"fitbit\"\n- Test that ProcessRelayMessage correctly upserts devices and updates last_seen and RSSI stats\n- Test ArchiveStale marks devices not seen for > 7 days as archived\n- Test person creation and device-to-person assignment API calls\n- Test MAC randomisation handling: two devices with same name and no simultaneous sighting are flagged as possible duplicates\n- Test that archived devices are excluded from GetDevices(false) but included in GetDevices(true)\n\n## Acceptance Criteria\n\n- Discovered BLE devices appear in the dashboard \"People and Devices\" panel within 30 seconds of first observation\n- Device type is auto-detected correctly for Apple, Fitbit, Garmin, and Samsung devices\n- User can assign labels and associate devices with named people via the dashboard UI\n- Drag-and-drop device-to-person assignment works in the UI\n- Devices not seen for > 7 days are automatically archived and hidden from the active list\n- Privacy limitation is clearly documented in the panel UI\n- Possible duplicate MAC-rotated devices are surfaced as merge suggestions\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"juliet","owner":"","created_at":"2026-03-28T01:44:02.204633291Z","created_by":"coding","updated_at":"2026-03-29T18:07:39.656772405Z","closed_at":"2026-03-29T18:07:39.656662663Z","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"],"dependencies":[{"issue_id":"spaxel-2wg","depends_on_id":"spaxel-c0q","type":"blocks","created_at":"2026-03-28T03:29:14.172209347Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-2x1w","title":"Implement command palette","description":"Ctrl+K universal search/command with fuzzy matching for power user efficiency.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"quebec","owner":"","created_at":"2026-04-10T02:03:09.482985749Z","created_by":"coding","updated_at":"2026-04-24T21:38:02.866241433Z","closed_at":"2026-04-24T21:38:02.866167874Z","close_reason":"Implemented command palette (Component 34): fuzzy search, 36 commands, time navigation, entity search, recent history, expert-mode gating. 94 tests passing.","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","mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} -{"id":"spaxel-32o","title":"Link weather diagnostics and repositioning advice","description":"## Background\n\nEven with good hardware and correct placement, some links will chronically underperform. A user who placed a node on a metal shelf, behind a TV, or in a corner will see consistently poor detection without understanding why. Telling users \"your detection quality is low\" is useless without telling them what to do about it. Link weather diagnostics provide root-cause analysis and specific, actionable repositioning advice — including 3D visualisation of why a link is performing poorly and where to move a node to fix it.\n\nThe name \"link weather\" is deliberate: just as weather forecasts present complex atmospheric state in human terms (\"partly cloudy with 60% chance of rain\"), link weather presents complex RF state as: \"Node A to Node B: interference detected. Likely cause: microwave oven or 2.4GHz congestion. Try moving Node B 1.5 metres to the right.\"\n\n## DiagnosticEngine\n\nNew module: mothership/internal/diagnostics/linkweather.go\n\nDiagnosticEngine runs as a background goroutine, consuming link health history from SQLite and emitting Diagnosis structs. It runs a full diagnostic pass every 15 minutes.\n\nA Diagnosis struct contains:\n- LinkID string\n- RuleID string (identifies which rule fired)\n- Severity: INFO, WARNING, ACTIONABLE\n- Title string (human-readable headline)\n- Detail string (explanation of the diagnosis in plain language)\n- Advice string (specific actionable steps)\n- RepositioningTarget *Vec3 (3D position to move the node to, or nil if repositioning is not the solution)\n- RepositioningNodeMAC string (which node to move)\n- ConfidenceScore float64 (how confident the diagnostic engine is in this diagnosis)\n\n## Diagnostic Rules\n\nRule 1: Environmental Change\nTrigger: High baseline drift (>5% per hour) correlated across multiple links simultaneously (>50% of active links).\nTitle: \"Environmental change detected\"\nDetail: \"Multiple sensing links are showing simultaneous baseline shifts. This typically indicates a temperature change, or a large object was moved in the space. The system is adapting automatically.\"\nAdvice: \"No action needed. The baseline will re-stabilise within 30 minutes.\"\nRepositioningTarget: nil\nConfidence: 0.85 if drift is correlated across >50% of links\n\nRule 2: WiFi Congestion or Distance\nTrigger: Packet rate health < 0.8 for more than 10 minutes on a single link.\nTitle: \"Node B has low signal rate\"\nDetail: \"Node [B] is only delivering [N]% of the expected [M] packets per second. The most common causes are distance from the WiFi router or congestion from nearby networks.\"\nAdvice: \"1. Move Node [B] within 10 metres of your WiFi router. 2. If already close, check if the 2.4GHz channel is congested (3+ networks on overlapping channels). 3. ESP32-S3 supports both 2.4GHz and 5GHz — if your router supports 5GHz, update Node B's WiFi config to use the 5GHz SSID.\"\nRepositioningTarget: nil (advice is router proximity, not specific coordinates)\n\nRule 3: Near-Field Metal Interference\nTrigger: Low phase stability (< 0.4) sustained for > 30 minutes during known-quiet periods.\nTitle: \"Metal interference near Node [A]\"\nDetail: \"The sensing link [A to B] has unstable phase measurements even when no one is moving. This is typically caused by metal objects in the near field of the node's antenna (within 10cm): metal shelves, radiators, TV backs, or large appliances.\"\nAdvice: \"Check for metal objects within 10cm of Node [A]. If Node [A] is on a metal surface or shelf, mount it on a non-metal bracket or wall. Try repositioning it 20-30cm away from metal surfaces.\"\nRepositioningTarget: nil (advice is clearance from metal, not a specific position)\n\nRule 4: Fresnel Zone Blockage (Half-Room Dead Zone)\nTrigger: Consistent miss rate (>30% of test walks that should be detected are missed) in a specific area of the room, AND the missing area correlates geometrically with an obstacle in the link's Fresnel zone.\nThis rule requires the feedback loop data (Phase 7, spaxel-i28) — specifically the user-submitted false negatives with position information. If no feedback data is available, this rule can trigger heuristically when one side of the room consistently shows lower blob confidence scores.\nTitle: \"Coverage gap detected — possible obstruction\"\nDetail: \"The area near [zone description] shows lower detection coverage. An obstacle may be blocking the path between Node [A] and Node [B], interrupting their sensing zone.\"\nAdvice: \"Move Node [B] [direction] by approximately [distance] to restore coverage. The target position is marked in green in the 3D view.\"\nRepositioningTarget: computed_optimal_position (see below)\n\nRule 5: Periodic Interference Spikes\nTrigger: Periodic spikes in deltaRMS variance (3-10 events per hour, each lasting 1-3 minutes) not correlated with occupancy data (no people detected moving).\nTitle: \"Periodic interference detected\"\nDetail: \"Node [A] to Node [B] is experiencing regular interference bursts [N] times per hour. This pattern is consistent with a microwave oven, a cordless phone, or a pulsed 2.4GHz source.\"\nAdvice: \"Consider the following: 1. Is Node [A] or Node [B] near a kitchen? Microwave ovens cause strong 2.4GHz interference. 2. A cordless DECT phone or baby monitor near one of the nodes may be the source. 3. Try moving the affected node at least 2 metres from any 2.4GHz appliances.\"\nRepositioningTarget: nil (interference is appliance-specific)\n\n## Repositioning Advice in 3D\n\nFor Rule 4 (Fresnel zone blockage), compute the optimal repositioning target:\n1. Use the GDOP-based coverage optimiser from Phase 5 self-healing fleet (spaxel-jc4) to compute the position that maximises GDOP for the blocked zone while keeping all other nodes fixed.\n2. The optimal position is the computed_optimal_position Vec3.\n3. In the 3D dashboard, render a \"ghost\" node at this position: translucent version of the node mesh, with a dashed line from the current position to the ghost position.\n4. Show expected GDOP improvement: \"Moving Node B here would improve detection in the east corner from [N]% to [M]%.\"\n\n## Weekly Reliability Trends\n\nStore daily health score averages in SQLite: link_health_daily (link_id TEXT, date DATE, avg_health REAL, min_health REAL, max_health REAL, PRIMARY KEY (link_id, date)).\n\nA background job runs daily at midnight and writes the day's health averages from the link health log (link_health_log table: link_id, timestamp, composite_score).\n\nDashboard shows for each link: 7-day sparkline of daily average health score. \"Best day\" annotation (highest average) and \"worst day\" annotation (lowest average). This gives users a sense of long-term reliability.\n\n## Files to Create or Modify\n\n- mothership/internal/diagnostics/linkweather.go: DiagnosticEngine and all 5 rules\n- mothership/internal/diagnostics/reposition.go: repositioning target computation\n- mothership/internal/health/linkhealth.go: add link_health_log table writes\n- dashboard/js/linkhealth.js: link health panel, diagnostics display, ghost node rendering\n- mothership/internal/dashboard/routes.go: GET /api/links/{id}/diagnostics, GET /api/links/{id}/health-history\n\n## Tests\n\n- Test Rule 1 (environmental change): inject simultaneous high-drift events across 60% of links, verify diagnosis fires with Severity=INFO\n- Test Rule 2 (WiFi congestion): inject packet_rate=0.7 for 15 minutes, verify diagnosis fires with appropriate advice text\n- Test Rule 3 (metal interference): inject phase_stability=0.3 for 35 minutes during a quiet window, verify diagnosis fires\n- Test Rule 4 (Fresnel blockage): requires feedback data — inject synthetic false-negative feedback events clustered in one spatial zone, verify diagnosis fires and RepositioningTarget is non-nil\n- Test Rule 5 (periodic interference): inject 5 deltaRMS variance spikes per hour for 2 hours, verify diagnosis fires with correct periodicity estimate\n- Test weekly trend aggregation: inject 7 days of health scores, verify daily averages are correctly computed and stored\n- Test that repositioning target is within room bounds and improves GDOP\n\n## Acceptance Criteria\n\n- All 5 diagnostic rules fire correctly on synthetic test data that matches their trigger conditions\n- Repositioning advice for Rule 4 appears as a ghost node in the 3D dashboard view\n- Expected GDOP improvement shown alongside repositioning ghost node\n- Weekly 7-day sparkline visible in link health panel for each link\n- Diagnostics accessible via API and displayed in Link Health panel on link click\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"juliet","owner":"","created_at":"2026-03-28T01:43:13.596164634Z","created_by":"coding","updated_at":"2026-03-29T18:07:39.683230580Z","closed_at":"2026-03-29T18:07:39.683089345Z","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"],"dependencies":[{"issue_id":"spaxel-32o","depends_on_id":"spaxel-axa","type":"blocks","created_at":"2026-03-28T03:29:14.023730499Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-35lb","title":"Add test-notification endpoint integration test","description":"Write integration test for test-notification endpoint fires correctly. Acceptance Criteria: Test endpoint integration test passes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-11T08:15:08.173375326Z","created_by":"coding","updated_at":"2026-04-11T10:40:05.106805090Z","closed_at":"2026-04-11T10:40:05.106665541Z","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:13","mitosis-child","mitosis-depth:1","parent-spaxel-40tl"]} -{"id":"spaxel-3ca","title":"Add time-travel debugging","description":"Implement:\n- Pause live mode\n- Timeline scrubbing\n- Replay 3D from recorded CSI data\n\nAcceptance: Can replay 24 hours of historical data with full 3D visualization.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-09T14:54:38.737598265Z","created_by":"coding","updated_at":"2026-04-09T17:41:51.707526513Z","closed_at":"2026-04-09T17:41:51.707468066Z","close_reason":"Time-travel debugging implementation complete. All acceptance criteria met:\n- Pause live mode: Implemented in dashboard/js/replay.js with Pause Live button\n- Timeline scrubbing: Full scrubber UI with seek functionality \n- Replay 3D from recorded CSI: Viz3D integration with enterReplayMode/exitReplayMode/updateReplayBlobs\n- 24-hour replay: Recording buffer supports 48-hour retention (exceeds requirement)\n\nBackend (mothership/internal/api/replay.go, replay/worker.go):\n- REST API for session management (start, stop, seek, tune, set-speed, set-state)\n- Separate signal processing pipeline for replay\n- Blob broadcasting to dashboard\n\nFrontend (dashboard/js/replay.js):\n- Complete replay controls UI\n- Parameter tuning panel with instant preview\n- Timeline loop polling session state\n\n3D Visualization (dashboard/js/viz3d.js):\n- Stores/restores live blob states during replay transitions\n- Full 3D blob rendering from replay data\n\nVerification: Comprehensive test suite exists (replay_test.go) covering session lifecycle, multiple sessions, parameter tuning, and timestamp parsing.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:20","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} -{"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":"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"]} -{"id":"spaxel-51k","title":"OTA firmware update system","description":"## Background\n\nOnce nodes are deployed in a home, they need to be updated without physical access. ESP-IDF has a mature OTA (Over-The-Air) update mechanism: two OTA flash partitions (factory, ota_0, ota_1 as defined in firmware/partitions.csv), HTTP download to the inactive partition, cryptographic verification, set boot partition, reboot. The mothership serves firmware binaries and triggers the update via a WebSocket downstream command. Phase 1 laid the groundwork in the firmware; this bead completes the mothership side.\n\n## What Already Exists\n\nfirmware/main/websocket.c has OTA command handling and an ota_download_task that handles the HTTP download to the inactive OTA partition. The partition table in firmware/partitions.csv has factory + ota_0 + ota_1 slots. The firmware parses {type:\"ota\", url:\"...\", md5:\"...\", version:\"...\"} downstream commands and initiates the download. What is missing is:\n- The mothership HTTP server for firmware binary serving\n- The REST API for triggering OTA per-node or fleet-wide\n- The firmware manifest for version management\n- The rollback detection logic on the mothership side\n- The dashboard UI for OTA management\n\n## Mothership Firmware Serving\n\nGET /firmware/latest or GET /firmware/{version}: serves the compiled .bin file from the /firmware volume mount. The response must include:\n- Content-Length header (required by ESP-IDF OTA HTTP client for progress reporting)\n- ETag header (MD5 of the binary, for caching)\n- Content-Type: application/octet-stream\n\nFirmware binaries are placed in the /firmware volume mount (configured in docker-compose.yml or k8s volume mount). The mothership reads the firmware manifest on startup and re-reads it when a new file appears (inotify watch or periodic re-scan every 60s).\n\n## Firmware Manifest\n\nFile: /firmware/manifest.json — auto-generated by the CI build process, or manually created.\nFormat: list of objects, each with version (semver string), filename (basename within /firmware/), md5 (hex string of binary MD5), size (integer bytes), build_timestamp (ISO8601).\nThe mothership's \"latest\" version is determined by sorting manifest entries by semver and taking the highest.\n\n## OTA Trigger API\n\nPOST /api/nodes/{mac}/ota — trigger OTA for a specific node\nRequest body: {\"version\": \"0.2.0\"} (optional; defaults to latest if omitted)\nResponse: {\"job_id\": \"abc123\", \"node_mac\": \"aa:bb:cc:dd:ee:ff\", \"target_version\": \"0.2.0\", \"status\": \"initiated\"}\n\nThe mothership looks up the node's active WebSocket session in the connection registry and sends the OTA command:\n{\"type\":\"ota\",\"url\":\"http://{mothership_ip}:{port}/firmware/0.2.0\",\"md5\":\"{hex_md5}\",\"version\":\"0.2.0\"}\n\nThe firmware immediately begins the download in a background task, sends ota_status messages ({\"type\":\"ota_status\",\"progress\":45,\"status\":\"downloading\"}) which the mothership logs and broadcasts to the dashboard.\n\n## Fleet Rolling Update\n\nPOST /api/ota/fleet — trigger OTA for all connected nodes\nRequest body: {\"version\": \"0.2.0\", \"stagger_seconds\": 30} (default stagger: 30s)\n\nThe rolling update coordinator in the mothership triggers OTA for the first node, waits stagger_seconds, then the next, and so on. This ensures:\n- Not all nodes reboot simultaneously (avoids a coverage gap window)\n- If a node fails OTA, the remaining nodes can be halted before more disruption\n- Fleet update progress is visible in dashboard per-node\n\nThe fleet update job is stored in SQLite (ota_jobs table) and survives mothership restarts.\n\n## Rollback Detection\n\nESP-IDF automatically rolls back to the previous firmware if the new image does not call esp_ota_mark_app_valid_cancel_rollback() within a boot window (the firmware does this on successful WebSocket connection to the mothership). The mothership detects rollback by comparing the firmware_version field in the hello message after OTA against the requested target version. If they differ, the mothership logs an OTA rollback event and updates the node's status to \"rollback\".\n\n## Dashboard OTA UI\n\nAdd an OTA panel to the dashboard settings or fleet page:\n- Per-node: current firmware version, available version (if newer), \"Update\" button\n- Fleet: \"Update All\" button with stagger slider, progress per node (with percentage from ota_status messages), last updated time per node\n- Version history: per-node firmware version history in tooltip or expandable row\n- Rollback indicator: nodes that rolled back are highlighted with a warning and the reason (if known)\n\n## Files to Create or Modify\n\n- mothership/internal/ota/server.go: firmware file serving with Content-Length and ETag\n- mothership/internal/ota/manifest.go: manifest parsing and latest-version logic\n- mothership/internal/ota/jobs.go: OTA job creation, fleet rolling update coordinator, status tracking\n- mothership/internal/dashboard/routes.go: register OTA API routes\n- dashboard/js/fleet.js or dashboard/js/ota.js: OTA UI panel\n\n## Tests\n\n- Test OTA command JSON serialisation matches firmware's expected format exactly\n- Test rolling update stagger timing with a mock time source (use a clock interface for testability)\n- Test that firmware version in hello message is parsed and stored in the node registry\n- Test manifest parsing: valid manifest, empty manifest, malformed manifest\n- Test rollback detection when hello version does not match target version\n\n## Acceptance Criteria\n\n- OTA command reaches firmware and triggers download (verified via ota_status messages)\n- Rolling update staggers correctly with the configured delay between nodes\n- After successful OTA, node reconnects with new firmware version in hello message\n- Rollback is detectable via hello version mismatch and displayed in dashboard\n- MD5 verification failure in firmware logs an error and the old firmware remains running\n- Fleet update status visible per-node in dashboard\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T01:37:32.472078279Z","created_by":"coding","updated_at":"2026-03-28T05:36:39.250035631Z","closed_at":"2026-03-28T05:36:39.249972673Z","close_reason":"Implemented: ota/manager.go + ota/server.go (fb69190) — HTTP firmware serving from /firmware volume, WebSocket-triggered OTA command, rolling update with 30s stagger, MD5 verification, firmware manifest.json, rollback detection via hello version mismatch","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-51k","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T03:29:13.874999678Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-54i","title":"load-shedding: add per-iteration timing and rolling avg to ProcessorManager","description":"## Task\nAdd pipeline timing and a 5-iteration rolling average to `ProcessorManager` in `mothership/internal/signal/processor.go`.\n\n## Changes to ProcessorManager struct (lines 221-228)\nAdd these fields:\n```go\ntype ProcessorManager struct {\n // ... existing fields ...\n iterDurations [5]time.Duration // ring buffer for last 5 iteration times\n iterIdx int // next write index (mod 5)\n iterCount int // how many values filled (0-5)\n shedLevel int // current load shedding level (0-3)\n steadyCount int // consecutive iters below recovery threshold\n}\n```\n\n## Changes to LinkProcessor.Process() (line 54)\nWrap the entire Process body to time it. At the START of Process():\n```go\nt0 := time.Now()\n```\nAt the END of Process(), before return, update the manager's ring buffer. BUT — Process is on LinkProcessor, not ProcessorManager. So instead, add timing to ProcessorManager.Process() (line 252):\n\n```go\nfunc (pm *ProcessorManager) Process(linkID string, ...) (*ProcessResult, error) {\n t0 := time.Now()\n // ... existing lock + delegate to processor ...\n result, err := processor.Process(payload, rssiDBm, nSub, recvTime)\n pm.mu.Unlock() // already have write lock\n elapsed := time.Since(t0)\n pm.updateShedding(elapsed)\n return result, err\n}\n```\n\n## New method updateShedding(elapsed time.Duration)\n```go\nfunc (pm *ProcessorManager) updateShedding(elapsed time.Duration) {\n pm.iterDurations[pm.iterIdx%5] = elapsed\n pm.iterIdx++\n if pm.iterCount < 5 { pm.iterCount++ }\n\n // compute rolling avg\n var sum time.Duration\n for i := 0; i < pm.iterCount; i++ {\n sum += pm.iterDurations[i]\n }\n avg := sum / time.Duration(pm.iterCount)\n\n // level up\n if avg >= 95*time.Millisecond && pm.shedLevel < 3 {\n pm.shedLevel = 3; pm.steadyCount = 0\n } else if avg >= 90*time.Millisecond && pm.shedLevel < 2 {\n pm.shedLevel = 2; pm.steadyCount = 0\n } else if avg >= 80*time.Millisecond && pm.shedLevel < 1 {\n pm.shedLevel = 1; pm.steadyCount = 0\n }\n\n // recovery: step down one level when avg < 60ms for 10 consecutive iters\n if avg < 60*time.Millisecond {\n pm.steadyCount++\n if pm.steadyCount >= 10 && pm.shedLevel > 0 {\n pm.shedLevel--\n pm.steadyCount = 0\n }\n } else {\n pm.steadyCount = 0\n }\n}\n```\n\n## New getter\n```go\nfunc (pm *ProcessorManager) GetShedLevel() int {\n pm.mu.RLock()\n defer pm.mu.RUnlock()\n return pm.shedLevel\n}\n```\n\nNote: `updateShedding` must NOT hold `pm.mu` because it's called after unlock. The iterDurations ring buffer is only written from `Process` so it is already serialized by the caller's lock sequence. Add a separate `mu` for the shed state or call updateShedding while still holding pm.mu — simplest: call it BEFORE Unlock, while still holding the write lock.\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-07T06:33:03.697771676Z","created_by":"coding","updated_at":"2026-04-07T16:53:42.209613205Z","closed_at":"2026-04-07T16:53:42.209404722Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} -{"id":"spaxel-5a3","title":"Add Fresnel zone visualization","description":"Implement wireframe ellipsoid overlay showing Fresnel zones between active links.\n\nAcceptance: 3D visualization shows ellipsoids between communicating nodes with proper scaling.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T14:54:38.915673399Z","created_by":"coding","updated_at":"2026-04-09T16:37:30.545014159Z","closed_at":"2026-04-09T16:37:30.544898040Z","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-sl2"]} -{"id":"spaxel-5es","title":"Ambient dashboard mode","description":"## Background\n\nA wall-mounted tablet showing who is home is a genuinely useful home appliance — a digital replacement for the \"whiteboard on the fridge\" that families have used for decades. The ambient mode is designed for exactly this use case: always on, minimal information density, visually calm, and unobtrusive. It should run for weeks without user interaction, surviving screen timeouts, browser updates, and mothership restarts. When something important happens (fall, security alert), it breaks the calm decisively to get attention.\n\n## Route and Renderer\n\nNew route: /ambient (separate from /simple and expert mode)\nThe user can set a specific device as \"ambient display\" via a browser bookmark or home screen shortcut.\n\nCanvas 2D renderer (not Three.js): the ambient mode uses a dedicated Canvas 2D rendering engine rather than Three.js. Reasons:\n1. Three.js is designed for 3D; the ambient view is a 2D top-down floor plan\n2. Canvas 2D uses significantly less GPU memory and power — important for always-on tablet use\n3. Simpler code path means fewer failure modes for long-running display\n4. Lower battery drain on iPad and Android tablets\n\nRenderer: dashboard/js/ambient_renderer.js. Renders via requestAnimationFrame at 2 Hz (one frame every 500ms). The 2 Hz update rate is intentional: it's visually smooth enough for a presence display and uses minimal CPU.\n\n## Rendered Elements\n\nBackground: colour depends on time of day (see Time-of-Day Palette below).\n\nRoom outline: 2D rectangles for each zone's bounding box (projected to floor plane). White (#ffffff) with 1px stroke, no fill (transparent interior). Zone labels: zone name in white text at centroid, 14px medium font.\n\nPortal lines: thin lines (0.5px, #a855f7 purple) across doorways.\n\nNode positions: small filled circles (radius 4px, #6b7280 grey, subtle).\n\nPerson blobs: filled circles (radius 10-18px) in person.color. Name label above: first name only, 12px white. Blob radius proportional to identity confidence: full size if confident, slightly smaller if low confidence. If anonymous: a ghost/outline circle (stroke only, no fill) in light grey.\n\nSystem status indicator: top-left corner. Small circle (8px radius): green (#22c55e) if all nodes healthy, amber (#f59e0b) if any node degraded, red (#ef4444) if any node offline. No text — just the dot. Tooltip on hover (for the rare touch-and-hold interaction).\n\nTime display: top-right. Current time in large readable font. 28px, #ffffff, tabular-nums font variant for clean time display without layout shifts.\n\nPerson positions: lerp-interpolated between WebSocket updates for smooth movement at the 2 Hz render rate. On each WebSocket message received, update the target position. On each render frame, move each person blob 20% of the remaining distance to the target position (exponential approach = naturally decelerating animation).\n\n## Auto-Dim\n\nWhen no person is detected in the room where the ambient tablet is physically located (a zone that the user has configured as the \"ambient display zone\"), reduce the canvas brightness after 60 seconds of no detection:\n- After 60s: reduce canvas globalAlpha to 0.4 (40% brightness)\n- Restore immediately when presence detected in the ambient zone again\n\nImplementation: Use the CSS filter brightness() property on the canvas element, animated with a CSS transition. Set `canvas.style.filter = 'brightness(0.4)'` after timeout. This approach correctly dims including text labels.\n\nOptionally: if the device supports it (iOS WKWebView + Power Saving APIs), the ambient mode can request Screen Wake Lock API to prevent the tablet display from sleeping. window.navigator.wakeLock.request('screen'). Re-request on visibility change (the lock is released when the page is hidden).\n\n## Alert Mode\n\nWhen a fall or security alert fires (FallDetected or AnomalyDetected event received via WebSocket):\n1. The Canvas render loop detects the alert event\n2. Full canvas background fades to #dc2626 (urgent red) over 500ms\n3. Large white text in the centre: \"FALL DETECTED — Alice\" or \"ALERT — Motion while away\"\n4. Pulsing animation: canvas background alternates between #dc2626 and #991b1b at 1 Hz\n5. Acknowledge button rendered as a large white rectangle in the centre below the text\n6. Tap/click on the acknowledge button: POST /api/fall/{id}/acknowledge or /api/anomalies/{id}/acknowledge. Returns to normal ambient mode.\n\nThis alert mode must be clearly visible from across the room — the text should be at least 48px on a typical 10-inch tablet.\n\n## Morning Briefing Overlay\n\nFirst time presence is detected after 6am (configurable): the ambient display shows a brief overlay card for 15 seconds:\n- Overlaid on the normal floor plan (dark semi-transparent background)\n- Sleep summary (if available): \"Alice: 7h 23m, good sleep\"\n- Today's expected departures (from presence prediction, Phase 7): \"Alice likely leaves at 8:30am\"\n- System status: \"4 nodes healthy\"\n\nAfter 15 seconds: fade out and return to normal ambient view. Tapping the overlay dismisses it immediately.\n\n## Time-of-Day Palette\n\nThe ambient canvas background colour shifts with the time of day to be visually appropriate:\n- 06:00-12:00 (morning): light blue-grey (#f0f4f8, near white) — cool morning light feel\n- 12:00-18:00 (afternoon): neutral grey (#1e293b, dark) — reduces eye strain in bright rooms\n- 18:00-22:00 (evening): warm amber-grey (#1c1507, very dark warm) — matches evening lighting\n- 22:00-06:00 (night): near black (#040404) — OLED-friendly, minimal light\n\nTransitions: smooth CSS gradient transition over 30 minutes at each boundary (not instant). Implemented by pre-computing the target colour for the next 30 minutes and using CSS linear-gradient + keyframe animation.\n\n## Performance\n\nTarget: < 5% CPU usage on a 2016-era iPad (A9 chip). Achieved by:\n- 2 Hz render rate instead of 60 Hz (30x reduction in GPU/CPU work)\n- No Three.js, WebGL, or shader compilation overhead\n- Canvas 2D is hardware accelerated but lightweight\n- Blob count in a home is typically 1-4 — trivial to render\n\nMemory: the ambient page should have < 50MB JS heap. No large textures, no complex geometry.\n\n## Files to Create or Modify\n\n- dashboard/ambient.html: minimal HTML shell\n- dashboard/js/ambient.js: main ambient mode logic, WebSocket connection, alert handling\n- dashboard/js/ambient_renderer.js: Canvas 2D rendering engine\n- dashboard/js/ambient_briefing.js: morning briefing overlay\n- mothership/internal/dashboard/routes.go: /ambient route served statically\n\n## Tests\n\n- Test Canvas 2D renderer draws correct shapes: zone rectangle at (1,1)-(3,3) appears as a white rectangle at the correct pixel coordinates (given known canvas size and room dimensions)\n- Test auto-dim timer: mock 60s with no presence event, verify canvas brightness is reduced\n- Test auto-dim restore: presence event arrives, verify brightness returns to 100%\n- Test alert mode: inject FallDetected event, verify canvas background changes to red and text appears\n- Test acknowledge clears alert mode and returns to normal\n- Test morning briefing overlay appears only once after 6am (localStorage flag set)\n- Test lerp interpolation: person position updates from (1,1) to (3,3), after 5 render frames should be approximately (2.5, 2.5) (with 20% step lerp)\n\n## Acceptance Criteria\n\n- Ambient mode runs for 7 days without page reload (no memory leaks, no uncaught exceptions)\n- Auto-dim activates after 60 seconds of no presence in the display zone\n- Fall/anomaly alert mode clearly visible from 3 metres away on a 10-inch tablet\n- Acknowledge button works and returns to normal ambient\n- Morning briefing overlay appears once per day, dismisses after 15s\n- Canvas 2D rendering consumes < 5% CPU on a mid-range tablet\n- Time-of-day palette transitions are smooth (no hard cuts)\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-03-28T02:00:34.796733529Z","created_by":"coding","updated_at":"2026-04-11T03:17:08.016688139Z","closed_at":"2026-04-11T03:17:08.016444040Z","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:6"],"dependencies":[{"issue_id":"spaxel-5es","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:14.888731706Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-5kf8","title":"Use FXAA instead of MSAA on mobile","description":"On screens < 1024px width, use FXAA antialiasing instead of MSAA if antialiasing is needed in renderer initialization.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"quebec","owner":"","created_at":"2026-04-11T06:59:34.231285657Z","created_by":"coding","updated_at":"2026-04-24T21:41:23.760079996Z","closed_at":"2026-04-24T21:41:23.759967487Z","close_reason":"Already implemented in commit 46b3495. FXAA post-processing is used instead of MSAA on screens < 1024px width. The renderer disables MSAA on mobile, and fxaa.js initializes Three.js EffectComposer with FXAAShader pass. Tests exist in mobile.test.js.","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-gufk"]} -{"id":"spaxel-5lo","title":"Implement Zones CRUD REST endpoints with OpenAPI docs","description":"Implement CRUD endpoints for zones: GET/POST /api/zones, PUT/DELETE /api/zones/{id}. Include OpenAPI-style godoc comments. Acceptance: endpoints respond correctly to HTTP requests, godoc annotations present.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T17:01:33.493352900Z","created_by":"coding","updated_at":"2026-04-07T18:13:38.639619498Z","closed_at":"2026-04-07T18:13:38.639505434Z","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:1","mitosis-child","mitosis-depth:1","parent-spaxel-0ii"]} -{"id":"spaxel-5yq","title":"load-shedding: health endpoint + dashboard WS alert integration","description":"## Task\nExpose the load shedding level in the health endpoint and send a dashboard WS alert when Level 3 is triggered. Requires spaxel-54i to be complete (GetShedLevel() must exist).\n\n## 1. Health endpoint (mothership/cmd/mothership/main.go)\nFind the `/healthz` handler (around line 218). Change the JSON to include `shedding_level`:\n```go\nfmt.Fprintf(w, `{\"status\":\"ok\",\"version\":\"%s\",\"shedding_level\":%d}`, version, pm.GetShedLevel())\n```\n\n## 2. Dashboard WS alert on Level 3\nIn the fusion loop in main.go (the goroutine that calls `pm.Process()`), track the previous shed level and broadcast an alert when it changes to 3 or recovers:\n```go\n// after pm.Process() call:\nnewLevel := pm.GetShedLevel()\nif newLevel != prevShedLevel {\n if newLevel == 3 {\n msg := map[string]interface{}{\n \"type\": \"alert\",\n \"severity\": \"warning\",\n \"description\": \"System under load — CSI rate reduced to 10 Hz\",\n }\n data, _ := json.Marshal(msg)\n dashboardHub.Broadcast(data)\n }\n prevShedLevel = newLevel\n log.Printf(\"[INFO] Load shedding level changed: %d\", newLevel)\n}\n```\nDeclare `prevShedLevel int` before the fusion goroutine.\n\n## 3. Level 3 rate reduction push (best effort — log only if push mechanism not yet available)\nWhen `newLevel == 3`, log: `log.Printf(\"[INFO] Load shed level 3 — would push 10Hz cap to nodes\")`\nWhen `newLevel` recovers from 3, log: `log.Printf(\"[INFO] Load shed recovered — restoring prior node rate\")`\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./...\n# curl http://localhost:8080/healthz should include shedding_level\n```\n\nRequires: spaxel-54i","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T06:33:19.278442007Z","created_by":"coding","updated_at":"2026-04-07T17:56:41.358181685Z","closed_at":"2026-04-07T17:56:41.358116981Z","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:4"],"dependencies":[{"issue_id":"spaxel-5yq","depends_on_id":"spaxel-54i","type":"blocks","created_at":"2026-04-07T06:33:23.206754212Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-65k","title":"Dashboard: activity timeline view","description":"## Overview\n\nBuild the unified activity timeline — the primary event history UI, accessible via the #timeline route added in the dashboard framework bead.\n\n## What to build (dashboard/js/timeline.js + timeline.css)\n\n### Timeline sidebar\n- Scrollable chronological event list (newest at top)\n- Event types with distinct icons/colors:\n - Zone entry/exit (green/orange)\n - Portal crossing (blue arrow)\n - Anomaly / security alert (red pulse)\n - Learning milestone (purple star)\n - System event (grey gear)\n- Each event shows: timestamp, description, person name (if identified), zone name\n- Click event → jump to that moment in the 3D view (triggers replay seek to that timestamp)\n\n### Filter bar\n- Filter by: person, zone, event type, time range (today / last 7d / custom)\n- Search box with debounced text filter across event descriptions\n\n### Inline feedback\n- Thumbs up / thumbs down on presence detection events\n- POST /api/feedback with event_id and correct (bool)\n- System response toast: 'Thanks — threshold adjusted for kitchen link'\n\n### Data source\n- Initial load: GET /api/events?limit=200&since=24h\n- Live updates: 'event' messages from WebSocket feed (requires spaxel-9eg)\n\n## Acceptance\n\n- 200 events render within 200ms of page load\n- New events prepend without layout shift\n- Clicking an event in replay mode seeks the replay to ±5s around the event\n- Feedback buttons POST successfully and show toast confirmation","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T12:56:03.195915329Z","created_by":"coding","updated_at":"2026-04-06T16:01:48.118589901Z","closed_at":"2026-04-06T16:01:48.118470381Z","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:1"]} -{"id":"spaxel-6ha","title":"Complete REST API: settings, zones, portals, triggers, notifications, replay","description":"## Problem\n\nMany HTTP endpoints are stubbed or missing. The dashboard settings panel, automation builder, and replay UI all require working REST endpoints.\n\n## Endpoints to implement in mothership/\n\n### Settings\n- GET /api/settings — return all configurable settings as JSON\n- POST /api/settings — update settings (partial update, merge semantics)\n\n### Zones & Portals\n- GET /api/zones — list all zones\n- POST /api/zones — create zone\n- PUT /api/zones/{id} — update zone geometry/name\n- DELETE /api/zones/{id} — delete zone\n- GET /api/portals — list all portals\n- POST /api/portals — create portal\n- PUT /api/portals/{id} — update\n- DELETE /api/portals/{id} — delete\n\n### Automation Triggers\n- GET /api/triggers — list all triggers\n- POST /api/triggers — create trigger\n- PUT /api/triggers/{id} — update\n- DELETE /api/triggers/{id} — delete\n- POST /api/triggers/{id}/test — fire trigger once for testing\n\n### Notifications\n- GET /api/notifications/config — get delivery channel config\n- POST /api/notifications/config — set Ntfy/Pushover/webhook settings\n- POST /api/notifications/test — send a test notification\n\n### Replay / Time-Travel\n- GET /api/replay/sessions — list available recording sessions\n- POST /api/replay/start — start replay at given timestamp\n- POST /api/replay/stop — stop replay, return to live\n- POST /api/replay/seek — seek to timestamp within session\n- POST /api/replay/tune — update pipeline parameters mid-replay\n\n### BLE Devices\n- GET /api/ble/devices — list known devices\n- PUT /api/ble/devices/{mac} — set label, assign to person\n\n## Acceptance\n\n- All endpoints return JSON with appropriate status codes\n- Settings endpoint persists to SQLite across restarts\n- Zone/portal CRUD reflected in the live 3D view within one WebSocket cycle\n- OpenAPI-style godoc comment on each handler with method, path, request, response","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-anthropic-sonnet-sp-20260504031710-0","owner":"","created_at":"2026-04-06T12:55:51.683246046Z","created_by":"coding","updated_at":"2026-05-04T03:54:25.379679073Z","closed_at":"2026-05-04T03:54:25.379679073Z","close_reason":"All required REST API endpoints fully implemented and tested: Settings GET/POST with SQLite persistence, Zones/Portals CRUD with WebSocket broadcast, Triggers CRUD with test-fire, Notifications config (ntfy/pushover/gotify/webhook/mqtt), Replay sessions with seek/tune/speed, BLE device management. go test and go vet pass clean.","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:9"],"dependencies":[{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-21n","type":"blocks","created_at":"2026-04-06T15:31:10.298537585Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-4fg","type":"blocks","created_at":"2026-04-06T15:31:10.528996520Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-kxf","type":"blocks","created_at":"2026-04-06T15:31:10.466981102Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-mul","type":"blocks","created_at":"2026-04-06T15:31:10.407580303Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-p5p","type":"blocks","created_at":"2026-04-06T15:31:10.594070369Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6ha","depends_on_id":"spaxel-ubu","type":"blocks","created_at":"2026-04-06T15:31:10.240906965Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-6hd","title":"Floor plan image upload and pixel-to-meter calibration","description":"## Overview\nAllow users to upload a floor plan image (PNG/JPG) and calibrate it to real-world coordinates so the 3D scene displays nodes and blobs at accurate physical positions.\n\n## Backend (mothership/internal/ — new floorplan.go)\n- POST /api/floorplan/image — multipart form; accept PNG/JPG max 10 MB; save to /data/floorplan/image.png\n- GET /api/floorplan/image — serve the stored image (200 or 404 if none)\n- POST /api/floorplan/calibrate — accept {ax,ay,bx,by,distance_m,rotation_deg}: two pixel coordinates and their real-world distance; compute and persist pixel-to-meter transform\n- GET /api/floorplan/calibrate — return current calibration or 404 if none\n- SQLite floorplan table: image_path TEXT, cal_ax,cal_ay,cal_bx,cal_by REAL, distance_m REAL, rotation_deg REAL, updated_at INT\n\n## Dashboard (dashboard/js/floorplan-setup.js)\n- Setup panel section: 'Floor Plan' with upload button\n- On image select: POST to /api/floorplan/image; display uploaded image on ground plane in 3D scene\n- Calibration UI: click point A on image → click point B → enter real-world distance in meters → Save\n- Compute pixel-to-meter scale factor: scale = distance_m / pixel_distance(A,B)\n- Apply scale and rotation to Three.js ground plane texture on load\n\n## Acceptance\n- Uploaded image displayed as ground plane texture in 3D view\n- Calibrated coordinate system maps pixel positions to correct meter positions\n- Image persists across server restart\n- > 10 MB upload rejected with 413 error","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-06T16:42:49.829463356Z","created_by":"coding","updated_at":"2026-04-09T12:45:23.190650793Z","closed_at":"2026-04-09T12:45:23.190526084Z","close_reason":"Implementation complete. All acceptance criteria met:\n- Backend API with image upload (max 10 MB) and calibration endpoints\n- Dashboard UI with two-point calibration and pixel-to-meter scale computation\n- Viz3D integration with texture transformation for accurate positioning\n- SQLite persistence for image metadata and calibration data\n- Image file storage at /data/floorplan/image.png\n\nBoth dependent beads (spaxel-dbd dashboard UI, spaxel-klk backend API) were already CLOSED.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1"],"dependencies":[{"issue_id":"spaxel-6hd","depends_on_id":"spaxel-dbd","type":"blocks","created_at":"2026-04-07T14:46:37.377627731Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-6hd","depends_on_id":"spaxel-klk","type":"blocks","created_at":"2026-04-07T14:46:37.307745453Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-6n9","title":"events: internal pub/sub event bus (decouple packages from dashboard)","description":"## Overview\nCreate a lightweight Go pub/sub event bus so any internal package can emit events without importing the dashboard package directly (part 2 of spaxel-2ap split).\n\n## Implementation in mothership/internal/eventbus/ (package already exists — extend it)\n\n```go\n// bus.go\npackage eventbus\n\ntype Event struct {\n Type string\n TimestampMs int64\n Zone string\n Person string\n BlobID string\n DetailJSON interface{}\n Severity string\n}\n\ntype Handler func(Event)\n\nvar (\n mu sync.RWMutex\n handlers []Handler\n)\n\nfunc Subscribe(h Handler)\nfunc Publish(e Event)\n```\n\n- `Publish` calls all subscribers in separate goroutines (non-blocking)\n- `Subscribe` is safe to call at any time, including after startup\n- Event types to define as constants: detection, zone_entry, zone_exit, portal_crossing, trigger_fired, fall_alert, anomaly, security_alert, node_online, node_offline, ota_update, baseline_changed, system, learning_milestone\n\n## Integration\n- Have the events package's `InsertEvent` also call `eventbus.Publish`\n- Dashboard WS handler subscribes to the bus to forward events to connected clients (wired in spaxel-9eg)\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/eventbus/\nPATH=$PATH:/home/coding/go/bin go test ./internal/eventbus/\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-06T22:31:07.051525693Z","created_by":"coding","updated_at":"2026-04-07T16:52:34.549568384Z","closed_at":"2026-04-07T16:52:34.549293102Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} -{"id":"spaxel-6th","title":"Multi-link CSI fusion and localization","description":"## Background\n\nSingle-link motion detection (Phase 2) shows presence on a link axis. With 4+ links we can localise people to ±0.5–1.0m using Fresnel zone weighted localization. This is the core spatial intelligence of spaxel. The physics: WiFi CSI is most sensitive to motion within the first Fresnel zone (an ellipsoid between TX and RX). The approach: for each occupancy grid voxel, compute its weight for each link based on Fresnel zone intersection, multiply by that link's deltaRMS, sum contributions, extract blob peaks.\n\n## What to Implement\n\nNew package: mothership/internal/fusion/\n\n### OccupancyGrid\n- mothership/internal/fusion/grid.go\n- 3D float32 grid, configurable resolution (default 0.25m)\n- Dimensions from room config (width, depth, height in meters)\n- Methods: Reset(), Set(x,y,z int, val float32), Get(x,y,z int) float32, Dims() (nx,ny,nz int)\n\n### Fresnel zone geometry\n- mothership/internal/fusion/fresnel.go\n- FresnelWeight(voxelPos, txPos, rxPos vec3, wavelength float64) float64\n- For 5GHz WiFi: wavelength = 0.06m\n- A voxel is in the first Fresnel zone if: d1+d2 <= dist(tx,rx) + wavelength/2\n where d1 = dist(voxel, tx), d2 = dist(voxel, rx)\n- Weight = deltaRMS × exp(-excess_path_length² / (2×0.1²))\n where excess_path_length = (d1+d2) - dist(tx,rx)\n- Weight = 0 outside Fresnel zone\n\n### FusionEngine\n- mothership/internal/fusion/engine.go\n- Inputs: ProcessorManager (from signal package), NodeRegistry (from fleet/session)\n- Runs at 10Hz via time.Ticker\n- Each tick: reset grid, for each active link get deltaRMS from ProcessorManager, for each voxel compute FresnelWeight × deltaRMS, accumulate to grid\n- Output: call BlobExtractor.Extract(grid), broadcast via dashboard hub as 'blob_update' JSON message\n\n### BlobExtractor\n- mothership/internal/fusion/blobs.go\n- Find 3D local maxima in the grid above threshold (default 0.02)\n- Non-maximum suppression: suppress any peak within 0.5m of a higher peak\n- Output: []BlobDetection{Position vec3, Confidence float32, Radius float32}\n- Limit to max 10 blobs\n\n### Room config\n- Add to mothership config (JSON): room.width_m, room.depth_m, room.height_m (defaults: 5, 5, 2.5)\n- Node positions: initially from fleet manager, defaulting to corners if unset\n\n## Key Files\n- mothership/internal/signal/processor.go — GetAllMotionStates()\n- mothership/internal/dashboard/hub.go — Broadcast() for blob_update\n- New: mothership/internal/fusion/grid.go, fresnel.go, engine.go, blobs.go + tests\n\n## Acceptance Criteria\n- FusionEngine produces blob_update WebSocket messages at 10Hz\n- Single active link produces blob peak along the TX-RX axis\n- Two crossing links produce peak near their intersection\n- BlobExtractor correctly suppresses nearby peaks\n- FresnelWeight returns 0 for voxels clearly outside the Fresnel zone\n- go test ./internal/fusion/... passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T03:30:50.362272102Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.188829209Z","closed_at":"2026-03-28T05:36:26.188507646Z","close_reason":"Implemented: fusion/fusion.go + fusion/grid3d.go (9c56a37) — 3D occupancy grid 0.25m res, Fresnel zone ellipsoid weighting, FusionEngine 10Hz, BlobExtractor with NMS","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-6th","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T03:30:50.362272102Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-70i","title":"Develop CSI simulator CLI","description":"Create Go CLI tool for:\n- Virtual node generation\n- Synthetic CSI binary frame output\n- Developer testing workflow\n\nAcceptance: CLI generates valid CSI binary frames for testing without hardware.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T14:54:38.869607224Z","created_by":"coding","updated_at":"2026-04-09T16:31:52.344734554Z","closed_at":"2026-04-09T16:31:52.344617250Z","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","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} -{"id":"spaxel-72t","title":"Provisioning payload and NVS write","description":"## Background\n\nWhen a new node is provisioned, it needs WiFi credentials (SSID + password) and a unique node ID (for use in the hello message and as a persistent identifier). The provisioning payload is assembled by the mothership and sent to the firmware over serial during onboarding, or can also be sent over the WebSocket after reconnect. Getting this right is foundational to the security and identity model of the entire system.\n\n## Why Mothership-Generated Node IDs?\n\nRather than generating a random ID on device, having the mothership assign node IDs allows it to: track provisioned-but-never-connected nodes for inventory management, support re-provisioning with ID continuity (same physical device gets same ID after factory reset), prevent ID collisions in multi-node deployments, and maintain a token-based security model where only provisioned nodes can connect.\n\n## NVS Schema\n\nThe firmware NVS schema is defined in firmware/main/spaxel.h (schema version 1). Keys and types:\n- wifi_ssid (string, max 32 chars)\n- wifi_pass (string, max 64 chars)\n- node_id (uint16, assigned by mothership)\n- node_token (string, 12-char hex, assigned by mothership)\n- mothership_host (string, empty = use mDNS)\n- mothership_port (uint16, default 8080)\n- role (uint8, 0=rx, 1=tx, 2=tx-rx, 3=passive)\n- sample_rate (uint16, default 20 Hz)\n- schema_ver (uint8, current = 1)\n\n## Mothership API\n\nPOST /api/provision\nRequest body: {\"ssid\": \"MyWifi\", \"password\": \"secret\", \"label\": \"Living Room Node\"}\nResponse: {\"node_id\": 42, \"provision_token\": \"a3f7b2c1d8e9\", \"config_blob\": \"{...json...}\"}\n\nThe config_blob is a JSON string encoding all NVS keys listed above. It is passed verbatim to the firmware over serial or WebSocket. The firmware parses it, writes each key to NVS, and reboots.\n\nExample config_blob:\n{\"wifi_ssid\":\"MyWifi\",\"wifi_pass\":\"secret\",\"node_id\":42,\"node_token\":\"a3f7b2c1d8e9\",\"mothership_host\":\"\",\"mothership_port\":8080,\"role\":0,\"sample_rate\":20,\"schema_ver\":1}\n\n## Firmware Provisioning Handling\n\nTwo provisioning paths:\n\n1. Serial provisioning (onboarding wizard path): Before WiFi is connected, the firmware listens on UART0 (115200 baud) for a JSON line starting with {\"provision\":. On receipt, write to NVS and reboot. This path works even before WiFi credentials are configured.\n\n2. WebSocket provisioning (re-provisioning path): A new downstream command type \"provision\" alongside existing role/config/ota/reboot in firmware/main/websocket.c. Allows the mothership to update credentials or reset a node over the air. This is useful for credential rotation without physical access.\n\n## Security Model\n\nThe provision_token is used as a bearer token in the WebSocket Authorization header (Authorization: Bearer ) for all subsequent connections. The mothership validates the token on every connection attempt against the provisioned_nodes SQLite table. Nodes without a valid token receive a {type:\"reject\"} downstream message and the connection is closed.\n\nToken format: 12 lowercase hex characters (48 bits of entropy). Generated server-side using crypto/rand. Not derivable from node_id or MAC address.\n\n## SQLite Storage\n\nAdd a provisioned_nodes table to the mothership SQLite database:\nCREATE TABLE provisioned_nodes (\n node_id INTEGER PRIMARY KEY,\n mac TEXT,\n token TEXT NOT NULL,\n label TEXT,\n provisioned_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n first_seen DATETIME,\n last_seen DATETIME,\n firmware_version TEXT,\n current_role INTEGER DEFAULT 0\n);\n\nThe mac field is populated when the node first connects and sends its hello message. Before first connection, mac is NULL (node is provisioned but not yet seen).\n\n## Implementation Location\n\n- mothership/internal/provision/handler.go: POST /api/provision handler\n- mothership/internal/provision/store.go: SQLite CRUD for provisioned_nodes\n- mothership/internal/ingestion/auth.go: WebSocket token validation on connection\n- firmware/main/websocket.c: add \"provision\" downstream command type\n- firmware/main/wifi.c: add serial JSON provisioning path on UART0 (pre-WiFi)\n\n## Re-provisioning\n\nIf a node already exists in provisioned_nodes (matched by mac), the mothership can re-provision it with a new token. The old token is invalidated immediately. The new config_blob is sent via the existing WebSocket connection (if online) or over serial (if physically accessible). This handles: WiFi password changes, mothership IP changes, node relabelling.\n\n## Tests\n\n- Test that POST /api/provision returns valid config_blob containing all required NVS keys\n- Test that node_id is unique and increments correctly\n- Test that token validation rejects connections with unknown tokens\n- Test that token validation rejects connections with expired/rotated tokens\n- Test NVS serialisation round-trip: parse config_blob back to NVS key-value map and verify all values\n- Test that a second provision for the same MAC updates rather than duplicates the record\n\n## Acceptance Criteria\n\n- Provisioned node connects to mothership successfully with the assigned node_id and token\n- Token validation correctly rejects unprovisioned connection attempts with {type:\"reject\"}\n- Node label stored and returned via GET /api/nodes in the node list\n- Re-provisioning updates token and invalidates old token within one round-trip\n- config_blob contains all required NVS keys with correct types\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-03-28T01:36:54.067220841Z","created_by":"coding","updated_at":"2026-03-28T05:36:39.268631627Z","closed_at":"2026-03-28T05:36:39.268468107Z","close_reason":"Implemented: provisioning/server.go (fb69190) + firmware/main/provision.c/h (fb69190) — POST /api/provision generates node_id+token+config_blob, UART serial provisioning window on ESP32, NVS write, provisioned_nodes SQLite table with token validation","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-72t","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T03:29:13.844135515Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-783","title":"Add Identify button to fleet status page","description":"Add 'Identify' button per row in the fleet status page that POSTs to /api/nodes/{mac}/identify.\n\n**Acceptance:**\n- Fleet status page has 'Identify' button per row","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","owner":"","created_at":"2026-04-09T11:11:49.991341754Z","created_by":"coding","updated_at":"2026-04-09T11:18:10.966551629Z","closed_at":"2026-04-09T11:18:10.966448973Z","close_reason":"Added Identify button (⚡) per row in the fleet status page that POSTs to /api/nodes/{mac}/identify. Button only shows for online nodes and sends a 5-second LED blink command with toast feedback.","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-h58"]} -{"id":"spaxel-7b3g","title":"Build ambient dashboard mode","description":"Create /ambient route for wall tablets with simplified top-down view and auto-dim functionality. Should run 7+ days continuously.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-10T02:03:09.350486770Z","created_by":"coding","updated_at":"2026-04-10T02:27:09.159574173Z","closed_at":"2026-04-10T02:27:09.159337207Z","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:3","mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} -{"id":"spaxel-7nk","title":"fix: sleep/handler.go cannot index interface{} map value","description":"## Problem\n`internal/sleep/handler.go` lines 229 and 232 fail with: `cannot index result[\"metrics\"] (map index expression of type interface{})`\n\nThe `result` map is of type `map[string]interface{}`, so `result[\"metrics\"]` returns `interface{}`, which cannot be directly indexed.\n\n## Fix\nAdd a type assertion before indexing. Around lines 229-232 in `internal/sleep/handler.go`:\n```go\n// Before the two if-blocks, get a typed reference:\nif metricsMap, ok := result[\"metrics\"].(map[string]interface{}); ok {\n if !metrics.SleepStartTime.IsZero() {\n metricsMap[\"sleep_start_time\"] = metrics.SleepStartTime.Format(\"15:04\")\n }\n if !metrics.SleepEndTime.IsZero() {\n metricsMap[\"sleep_end_time\"] = metrics.SleepEndTime.Format(\"15:04\")\n }\n}\n```\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/sleep/\n```","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T22:30:05.128489582Z","created_by":"coding","updated_at":"2026-04-06T22:40:47.430043249Z","closed_at":"2026-04-06T22:40:47.429779459Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} -{"id":"spaxel-7qo","title":"Dashboard: WebSocket reconnection with exponential backoff and state management","description":"## Overview\nImplement robust client-side WebSocket reconnection with exponential backoff, jitter, and visual state transitions during disconnects.\n\n## Reconnection logic (dashboard/js/websocket.js or app.js)\n- Backoff sequence: 1s, 2s, 4s, 8s, max 10s; ±500ms random jitter on each attempt\n- Track disconnect_duration_ms from first disconnect event\n\n## Visual state transitions:\nDisconnect < 5s: silent (no UI change); blob positions extrapolated from last velocity\nDisconnect 5-30s: 3D scene dims to 50% opacity; 'Reconnecting...' spinner in status bar; user interaction disabled\nDisconnect > 30s: non-blocking modal: 'Connection lost — [Reload Page]'; allow viewing stale scene\n\n## Blob position extrapolation (<5s):\n- On disconnect: record last_position and last_velocity per blob\n- Each animation frame: position = last_position + last_velocity × elapsed_s (capped at 2s extrapolation)\n\n## On successful reconnect:\n- Clear all blob trails (path history lines)\n- Apply snapshot from first WebSocket message (spaxel-fll)\n- Restore scene opacity to 100%\n- Dismiss spinner and modal\n- Log 'Reconnected after Xs' to console\n\n## Acceptance\n- Disconnect for 3s: no visual change; blobs continue moving smoothly\n- Disconnect for 10s: scene dims, spinner shown\n- Disconnect for 35s: modal appears; scene still visible\n- Reconnect: modal dismissed, trails cleared, scene snaps to current state within 200ms\n- Requires: spaxel-fll (snapshot protocol), spaxel-896 (panel framework)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"echo","owner":"","created_at":"2026-04-06T16:44:33.446200584Z","created_by":"coding","updated_at":"2026-04-07T16:45:31.143915724Z","closed_at":"2026-04-07T16:45:31.143823228Z","close_reason":"WebSocket reconnection with exponential backoff and visual state management was already fully implemented in commit ff3428f. All acceptance criteria met: backoff 1s-10s with ±500ms jitter, <5s silent extrapolation, 5-30s dimming with spinner, >30s modal with reload button, reconnect clears trails and restores scene from snapshot.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred"]} -{"id":"spaxel-7x2","title":"Wire anomaly detection & security mode API endpoints","description":"## Backend\n\n- Confirm AnomalyDetector is initialized and running in main()\n- Anomaly events must be pushed to the dashboard WS feed as 'alert' messages\n- GET /api/anomalies?since=24h — list recent anomaly events\n- POST /api/security/arm + /api/security/disarm — arm/disarm security mode\n- GET /api/security/status — { armed, learning_until, anomaly_count_24h }\n\n## Acceptance\n- Endpoints return correct JSON structure\n- Anomaly events push to WS feed as 'alert' messages\n- Arm/disarm state persists across server restarts","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T16:09:35.812256758Z","created_by":"coding","updated_at":"2026-04-07T19:17:55.756354374Z","closed_at":"2026-04-07T19:17:55.756259632Z","close_reason":"All acceptance criteria verified and already committed in b1c2218. AnomalyDetector initialized in main() with 6h periodic updates. Anomaly events broadcast to dashboard WS as alert messages. GET /api/anomalies?since=24h, POST /api/security/arm, POST /api/security/disarm, GET /api/security/status all wired and tested. Arm/disarm state persisted to learning_state table and restored on restart. All 14 related tests passing.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:2","mitosis-child","mitosis-depth:1","parent-spaxel-a55"]} -{"id":"spaxel-7zy","title":"Anomaly detection and security mode","description":"## Background\n\nAfter 7+ days of learning, spaxel knows the household's normal patterns: who is home when, which zones are occupied at which hours, which BLE devices are typically present. Deviations from these patterns — unexpected late-night presence, unknown BLE devices, motion during away mode — can indicate security events. Security mode explicitly arms anomaly detection with immediate alerts and a comprehensive alert chain, transforming spaxel into a basic home security system.\n\n## Normal Behaviour Model\n\nAnomalyDetector maintains a statistical model of normal behaviour per (hour_of_week, zone_id) slot. For each slot:\n- expected_occupancy: whether this zone is typically occupied at this time (fraction of historical samples that had occupancy > 0)\n- typical_person_count: mean occupant count\n- typical_ble_devices: set of BLE device MACs typically present (with minimum frequency threshold: seen in > 50% of this hour_of_week slot)\n\nThe model is updated weekly from the activity history and zone transition history. A minimum of 7 days of data is required before anomaly detection activates. Before 7 days: no anomaly alerts fire.\n\nNew file: mothership/internal/analytics/anomaly.go\n\n## Anomaly Scoring\n\nFour anomaly types, each with a base score contribution:\n\n1. Unusual hour presence (score: 0.7 by default, 0.9 in security mode):\n Motion detected in zone Z at hour H, but expected_occupancy for (H, Z) < 0.1 (this zone is empty >90% of the time at hour H historically). Apply time-of-day sensitivity: late night (00:00-06:00) has 1.5x multiplier.\n\n2. Unknown BLE device (score: 0.5 by default, 0.8 in security mode):\n BLE device with RSSI > -60 dBm (close range) that is not in the registered device list AND has not been seen before (not even in the archive). If it was seen once before but not regularly, score = 0.3.\n\n3. Motion during away mode (score: 0.95, always immediate):\n Any presence detected when SystemMode == AWAY. By definition anomalous — all registered people are absent. This always fires an alert regardless of model training status.\n\n4. Unusual dwell duration (score: 0.4):\n Person present in zone for > 5x the historical mean dwell time for that (person, zone, hour_of_week) combination. May indicate a person is incapacitated (fell and can't get up) rather than a security event — cross-check with fall detection before escalating.\n\nComposite anomaly score: max(individual scores) for the most anomalous concurrent event. Alert threshold: score > 0.6 (default mode), score > 0.4 (security mode).\n\n## Security Mode\n\nSecurityMode is an extension of the SystemMode. When SystemMode is AWAY:\n- All anomaly detection thresholds are lowered (as per scores above)\n- Alert chain is immediate (no T+2min or T+5min delays — fires immediately)\n- Quiet hours are suppressed (all alerts bypass quiet hours when in security mode)\n- All four anomaly types are active regardless of model training status (even before 7 days)\n\nAuto-away detection:\n- Condition: all registered person_ids have had no BLE device seen by any node for > 15 minutes\n- On condition met: set SystemMode = AWAY. Log: \"Auto-away activated — all BLE devices absent\"\n- Broadcast system_mode_change WebSocket event to dashboard\n\nAuto-disarm:\n- Condition: any registered person's BLE device seen with RSSI > -70 dBm at any node\n- On condition met: set SystemMode = HOME. Clear security alerts (or keep them acknowledged-pending).\n- Broadcast system_mode_change event\n- Show \"Welcome home\" card in dashboard if identity is known: \"Alice arrived home.\"\n\nManual override: dashboard has a Home/Away/Sleep toggle that overrides auto-detection. Once manually set, auto-detection is paused for 30 minutes (avoids immediate re-trigger).\n\n## Alert Chain\n\nOn anomaly score > threshold:\n\n1. T+0s: Dashboard alarm overlay (red full-screen banner, z-index: top):\n \"Anomaly detected: [description]. [Acknowledge] [View in 3D] [Dismiss]\"\n Description examples: \"Motion detected in Kitchen at 3:12am (unusual hour)\", \"Unknown device detected near front door\", \"Motion detected while everyone is away\"\n\n2. T+0s (security mode) or T+30s (normal mode): push notification via configured channel with floor-plan thumbnail (using notification module, spaxel-zpt)\n\n3. T+0s (security mode) or T+2min (normal mode): webhook/MQTT via automation engine (trigger type: anomaly)\n\n4. T+5min (without acknowledgement): escalation webhook (secondary URL in settings)\n\nAcknowledge: acknowledges the alert, logs in activity timeline with current time, stops escalation timers. Shows brief form: \"What was this?\" — Expected/known event / Genuine intrusion / False alarm. This feeds the false positive rate for the anomaly model.\n\n## Detection History and Visualisation\n\nAll anomaly events are logged in the activity timeline (Phase 8). The 3D view adds an \"Anomaly\" layer:\n- When an active unacknowledged anomaly exists, the relevant zone pulses with a red overlay in the 3D scene\n- Anomaly events in the timeline are marked with a red shield icon\n\nWeekly anomaly summary: \"0 anomalies this week\" (reassuring) or \"3 anomalies detected: 2 false alarms, 1 unacknowledged.\"\n\n## Files to Create or Modify\n\n- mothership/internal/analytics/anomaly.go: AnomalyDetector, normal behaviour model\n- mothership/internal/fleet/manager.go: SystemMode integration, auto-away detection, BLE presence tracking for auto-disarm\n- dashboard/js/anomaly.js: alarm overlay, acknowledge UI\n- mothership/internal/dashboard/routes.go: GET /api/mode, POST /api/mode, GET /api/anomalies/history\n- mothership/internal/events/events.go: AnomalyEvent type\n\n## Tests\n\n- Test anomaly score for \"unusual hour presence\": expected_occupancy=0.05 at 3am -> score fires\n- Test \"unknown BLE device\": inject device MAC not in registry at RSSI -55 -> anomaly fires\n- Test \"motion during away\": set SystemMode=AWAY, inject presence event -> immediate alert fires regardless of thresholds\n- Test auto-away: all BLE devices absent for 900s -> SystemMode becomes AWAY\n- Test auto-disarm: device seen at RSSI=-65 -> SystemMode becomes HOME\n- Test alert chain timing in normal mode: alert at T+0, notification at T+30s, webhook at T+2min\n- Test security mode immediate alert chain: all three fire at T+0\n- Test acknowledgement cancels pending escalation timers\n\n## Acceptance Criteria\n\n- Anomaly fires correctly for unexpected late-night motion after 7 days of baseline\n- Security mode auto-activates when all registered BLE devices absent for 15 minutes\n- Alert chain fires in correct sequence for both normal and security mode\n- Auto-disarm triggers correctly when first registered BLE device returns\n- Dashboard alarm overlay is clearly visible (full-screen red banner) on anomaly detection\n- Zone pulsing in 3D view during active unacknowledged anomaly\n- Acknowledgement and feedback form records correctly\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-03-28T01:53:44.888473549Z","created_by":"coding","updated_at":"2026-04-09T13:49:09.454132371Z","closed_at":"2026-04-09T13:49:09.453996847Z","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:13"]} -{"id":"spaxel-896","title":"Build dashboard panel/modal/sidebar UI framework","description":"## Problem\n\nThe dashboard is currently a single live 3D view with no panel system. All Phase 6-9 UI work (automation builder, timeline, explainability, settings, notifications, presence predictions) requires a panel/sidebar framework to hang on.\n\n## What to build\n\n### Panel System (dashboard/js/panels.js)\n- Slide-in sidebar (right, 360px) with close button and title\n- Modal overlay (centered, 600px wide) for forms and wizards\n- Toast notification stack (bottom-right)\n- Panel registry: panels can be opened by name from anywhere in the app\n\n### Route/Mode Navigation (dashboard/js/router.js)\n- Hash-based routing: #live (default), #timeline, #automations, #settings, #ambient, #replay\n- Mode toggle bar in the header: Live | Timeline | Automations | Settings\n- Active mode preserved across page refresh (localStorage)\n\n### State Management (dashboard/js/state.js)\n- Central app state object (nodes, blobs, zones, links, alerts, events, ble_devices, triggers)\n- Subscribe/notify pattern for components to react to state changes\n- Separate from WebSocket message parsing\n\n### Settings Panel (dashboard/js/settings-panel.js)\n- Motion threshold slider\n- Sensing rate override\n- Notification channel config (Ntfy URL, Pushover token)\n- System info (version, uptime, node count)\n\n## Acceptance\n\n- Panel opens/closes smoothly with CSS transitions\n- Route changes update the active view without page reload\n- Settings panel reads from GET /api/settings and saves via POST /api/settings\n- All existing 3D live view functionality unaffected","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T12:55:28.903260636Z","created_by":"coding","updated_at":"2026-04-06T14:08:18.251230378Z","closed_at":"2026-04-06T14:08:18.250924137Z","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"]} -{"id":"spaxel-8fr5","title":"Implement hamburger menu for mobile expert mode","description":"On screens < 1024px width, replace always-visible side panels with a hamburger menu. Implement transform-based slide-in animation with overlay backdrop.\n\n**Files:** dashboard/index.html (hamburger button markup), dashboard/css/expert.css (media queries, animations)\n\n**Acceptance Criteria:**\n- Hamburger button appears in header on screens < 1024px (44x44px touch target)\n- Opening menu slides left panel in with translateX(0) animation (200ms ease-out)\n- Overlay backdrop is semi-transparent and closes menu on tap\n- Menu contains: Node List, Link List, Presence Panel, Timeline, people/devices panel\n- Active tab opens first (last-used panel)\n- Close button (X, 44px) in top-right of menu\n- Menu closes on Escape key","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-11T06:26:50.014866583Z","created_by":"coding","updated_at":"2026-04-11T06:49:22.893642711Z","closed_at":"2026-04-11T06:49:22.893464912Z","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","mitosis-child","mitosis-depth:1","parent-spaxel-kth"]} -{"id":"spaxel-8ke","title":"Implement activity timeline","description":"Build a universal event stream visualization with:\n- Tap-to-jump navigation\n- Inline feedback display\n\nAcceptance: Users can view all system events chronologically and tap any event to jump to that moment in time.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"golf","owner":"","created_at":"2026-04-09T14:54:38.600997561Z","created_by":"coding","updated_at":"2026-04-09T15:08:10.262940103Z","closed_at":"2026-04-09T15:08:10.262798427Z","close_reason":"Implemented activity timeline with tap-to-jump navigation and inline feedback display.\n\n**Features implemented:**\n- Tap-to-jump navigation: Click any event to create replay session and seek to that moment in time\n- Inline feedback display: Thumbs up/down buttons on each event for detection feedback\n- Replay API integration: Creates replay window around event timestamp (±5 seconds)\n- Feedback API: New /api/feedback endpoint for correct/incorrect/missed detection reports\n- Real-time event insertion: New events appear via WebSocket with animation\n- Filter UI: Type, zone, person, time range, and search filters\n- Load more pagination: Keyset cursor-based pagination for large event sets\n\n**Acceptance criteria met:**\n- Users can view all system events chronologically\n- Tap any event to jump to that moment in time via replay mode\n- Inline feedback buttons allow marking detections correct/incorrect","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"]} -{"id":"spaxel-8u3","title":"Fleet manager with SQLite persistence","description":"Node registry, role assignment engine, and self-healing.\n\n## Deliverables\n- New package: mothership/internal/fleet/\n- SQLite node registry (MAC, ID, role, last seen, health, position)\n- Role assignment engine (TX/RX/passive/TX-RX including passive radar virtual node)\n- Stagger scheduling for multi-node packet timing\n- Self-healing: auto role reassignment on node loss, graceful degradation warnings\n- REST API endpoints: GET /api/nodes, GET /api/nodes/:mac, POST /api/nodes/:mac/role\n\n## Acceptance Criteria\n- Node state persists across mothership restarts\n- Roles auto-reassign when a node goes offline\n- Stagger scheduling prevents packet collisions\n- Tests cover registration, role assignment, and failure recovery\n\n## References\n- Plan: docs/plan/plan.md items 14\n- SQLite: modernc.org/sqlite (pure Go, already in go.mod intent)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","owner":"","created_at":"2026-03-27T01:56:38.835804826Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.132787526Z","closed_at":"2026-03-28T05:36:26.132727724Z","close_reason":"Implemented: fleet/manager.go + fleet/registry.go (fb69190) — SQLite node registry, role assignment engine, stagger scheduling, self-healing role reassignment on node loss","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","dependencies":[{"issue_id":"spaxel-8u3","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T03:29:13.704767150Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-8wem","title":"Increase touch target sizes to 44x44px minimum","description":"Audit and resize all interactive elements in expert mode for WCAG 2.1 touch target compliance (44x44px minimum).\n\n**Files:** dashboard/css/expert.css\n\n**Acceptance Criteria:**\n- Layer toggle checkboxes have 44x44px touch area (padding or pseudo-element expansion)\n- Link list entries have minimum 44px height\n- Panel close buttons are 44px x 44px\n- Slider controls (baseline tau, threshold) have drag targets at least 44px tall\n- Context menu items have minimum 44px height","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-11T06:26:50.115125378Z","created_by":"coding","updated_at":"2026-04-11T06:54:59.059288468Z","closed_at":"2026-04-11T06:54:59.058954568Z","close_reason":"Implemented WCAG 2.1 touch target compliance (44x44px minimum) for all expert mode interactive elements: Panel close buttons expanded to 44x44px, slider controls expanded to 44px height, toggle switches expanded to 44px height, checkboxes expanded with pseudo-elements, context menu items and link entries set to minimum 44px height, all buttons meet 44x44px minimum.","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-kth"]} -{"id":"spaxel-9eg","title":"Expand dashboard WebSocket feed: events, alerts, anomalies, triggers, BLE","description":"## Problem\n\nThe dashboard WebSocket (/ws/dashboard) currently only sends blob/node/zone/link/confidence/predictions/sleep/flow state. Events, alerts, anomalies, triggers, and BLE device data are never pushed to the dashboard, making Phase 6-9 UI impossible without a polling API.\n\n## What to add to the WS feed\n\nIn mothership/internal/dashboard/ (hub.go or server.go):\n\n### New message types to broadcast:\n\n**event** — presence transitions, zone entries/exits, portal crossings\n { type: 'event', event: { id, ts, kind, zone, blob_id, person_name } }\n\n**alert** — anomaly detections, security mode triggers\n { type: 'alert', alert: { id, ts, severity, description, acknowledged } }\n\n**ble_scan** — BLE device list updates (5s interval)\n { type: 'ble_scan', devices: [{ mac, name, rssi, last_seen, label, blob_id }] }\n\n**trigger_state** — automation trigger state changes\n { type: 'trigger_state', trigger: { id, name, last_fired, enabled } }\n\n**system_health** — periodic system stats (60s interval)\n { type: 'system_health', health: { uptime_s, node_count, bead_count, go_routines, mem_mb } }\n\n### In dashboard JS (app.js):\n- Handle each new message type in the WebSocket onmessage handler\n- Update app state for each type\n- Log unhandled types to console (for future debugging)\n\n## Acceptance\n\n- All 5 new message types appear in browser devtools WebSocket inspector\n- BLE device list updates every 5s when devices are present\n- Events appear within 1s of a zone transition\n- Existing blob/node/link messages unaffected","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"delta","owner":"","created_at":"2026-04-06T12:55:40.859267153Z","created_by":"coding","updated_at":"2026-04-07T15:20:38.722641396Z","closed_at":"2026-04-07T15:20:38.722538891Z","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:1"],"dependencies":[{"issue_id":"spaxel-9eg","depends_on_id":"spaxel-28k","type":"blocks","created_at":"2026-04-06T14:18:27.421346709Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-9eg","depends_on_id":"spaxel-2ea","type":"blocks","created_at":"2026-04-06T14:18:27.498282335Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-9eg","depends_on_id":"spaxel-fyi","type":"blocks","created_at":"2026-04-06T14:18:27.643320410Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-9eg","depends_on_id":"spaxel-hf8","type":"blocks","created_at":"2026-04-06T14:18:27.581630865Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-9eg","depends_on_id":"spaxel-ncw","type":"blocks","created_at":"2026-04-06T14:18:27.696083142Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-9mkk","title":"Fix Three.js OrbitControls touch event handling","description":"Fix touch event propagation from panels to canvas, resolve iOS Safari passive event listener warnings, prevent double-tap zoom conflicts, improve pinch gesture accuracy, and enable three-finger pan.\n\n**Files:** dashboard/js/controls.js, dashboard/css/expert.css\n\n**Acceptance Criteria:**\n- Touch events on sidebar panels do not propagate to the canvas (event.stopPropagation() on panel touch listeners)\n- No iOS Safari passive event listener warnings (touch-action: none on canvas or non-passive listener override)\n- Double-tap to zoom is disabled (user-scalable=no in meta viewport)\n- Pinch gesture is accurate on actual devices (zoomSpeed may be adjusted for touch)\n- Three-finger pan is enabled in OrbitControls","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","owner":"","created_at":"2026-04-11T06:26:49.932473223Z","created_by":"coding","updated_at":"2026-05-04T06:27:19.788646016Z","closed_at":"2026-05-04T06:27:19.788646016Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:2","mitosis-child","mitosis-depth:1","parent-spaxel-kth"],"dependencies":[{"issue_id":"spaxel-9mkk","depends_on_id":"spaxel-yvwf","type":"blocks","created_at":"2026-04-11T06:34:56.656196392Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-9nj","title":"fix: falldetect unused imports and vars cause build failure","description":"## Problem\n`internal/falldetect/detector.go` fails to compile:\n- Line 9: `\"math\"` imported and not used\n- Line 360: `startZ` declared and not used\n- Line 360: `endZ` declared and not used\n\n## Fix\n1. Remove `\"math\"` from the import block in `detector.go`\n2. Remove or use the `startZ` and `endZ` variables (prefix with `_` if needed, or delete)\n\n## Verify\n```bash\ncd /home/coding/spaxel/mothership && PATH=$PATH:/home/coding/go/bin go build ./internal/falldetect/\n```\nMust compile with no errors.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-04-06T22:29:46.582450658Z","created_by":"coding","updated_at":"2026-04-06T22:46:04.704272938Z","closed_at":"2026-04-06T22:46:04.704068691Z","close_reason":"Already fixed in commit d3f4d8f — removed unused math import and startZ/endZ variables. Build verified clean.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:2"]} -{"id":"spaxel-9z3","title":"Infrastructure: database schema migration engine","description":"## Overview\nImplement a versioned schema migration framework so the mothership can safely upgrade its SQLite schema across releases without data loss.\n\n## Implementation (mothership/internal/db/ or cmd/mothership/migrate.go)\n\n- schema_migrations table: version INT PK, applied_at INT, description TEXT\n- Migration registry: slice of Migration structs {Version int, Description string, Up func(*sql.Tx) error}\n- Startup phase (before any other subsystem): read current schema_ver; identify pending migrations\n- Run each pending migration in its own transaction; commit on success, rollback on failure\n- On failure: log error, preserve pre-migration backup, exit non-zero\n- Startup shutdown on error: never serve traffic with a partially migrated schema\n\n## Pre-migration safety\n- Before first migration: use SQLite Online Backup API to copy DB to /data/backups/pre-upgrade-v-to-v-.sqlite\n- Create /data/backups/ if not exists\n- Backups older than 90 days: pruned daily at 02:00 local time (or on startup)\n\n## Initial migrations\n- migration_001: initial schema (nodes, links, baselines, events, zones, portals, sessions, etc.)\n- migration_002: add diurnal_baselines table\n- migration_003: add anomaly_patterns table\n- migration_004: add prediction_models table\n- migration_005: add ble_device_aliases table\n\n## Acceptance\n- 'go test ./internal/db/...' passes including migration test from v1 to current\n- Pre-migration backup created before any schema change\n- Failed migration exits cleanly; DB unchanged; backup preserved\n- Idempotent: running migrations on already-migrated DB is a no-op","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":1,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T13:09:15.158103223Z","created_by":"coding","updated_at":"2026-04-06T15:10:12.224579147Z","closed_at":"2026-04-06T15:10:12.224316659Z","close_reason":"Database schema migration engine implemented with:\n\n- schema_migrations table tracking version, applied_at, description\n- Migration registry with Migration structs (Version, Description, Up func)\n- Startup phase integration via OpenDB() with 7-phase sequence\n- Transactional migration execution with commit/rollback\n- Pre-migration backup using VACUUM INTO to /data/backups/\n- Failed migration exits cleanly with backup preserved\n- Idempotent migrations (checks current version, only runs pending)\n- 90-day backup retention with automatic pruning\n- Initial migrations 001-005 for all core tables\n- Comprehensive test coverage (idempotency, rollback, v1-to-current migration)\n\nImplementation in mothership/internal/db/ (migrate.go, migrations.go, db.go, migrate_test.go)\nCommitted as 2da6e23 and deadlock fix as f9632c7","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["deferred","failure-count:2"]} -{"id":"spaxel-9zs","title":"Mothership: SIGTERM graceful shutdown sequence","description":"## Overview\nImplement the full 10-step ordered shutdown sequence so the mothership drains cleanly without data loss on SIGTERM (Docker stop, Kubernetes termination).\n\n## Sequence (plan lines 3480-3494) — 30s hard deadline:\n1. Set shutting_down=true; ingestion server returns HTTP 503 to new WebSocket upgrade requests\n2. Broadcast {type:'shutdown', reconnect_in_ms:30000} to all dashboard WebSocket clients\n3. Cancel fusion loop context (stops fusion goroutine)\n4. Drain signal processing pipeline: wait for in-flight CSI frames (max 2s)\n5. Flush in-memory baselines to SQLite in a single transaction\n6. Sync CSI recording buffer to disk (close writer, fsync)\n7. Close all node WebSocket connections with normal close frame (1000)\n8. Write {type:'system', description:'Mothership stopped'} event to events table\n9. PRAGMA wal_checkpoint(FULL) to collapse WAL into main DB file\n10. sqlite3.Close()\n\n## Implementation\n- context.WithTimeout(30s) wraps entire shutdown\n- Each step gets its own log line: '[SHUTDOWN] Step N/10 — ...'\n- Steps that fail log ERROR but do not abort remaining steps\n- Exit code 0 if all steps completed within deadline; exit code 1 if deadline exceeded\n\n## Acceptance\n- docker stop (SIGTERM) completes within 35s (30s shutdown + 5s buffer)\n- No WAL file remains after clean shutdown (verified with ls -la /data/)\n- system_stopped event present in events table after restart\n- In-flight CSI frames processed (not dropped) during drain step","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","owner":"","created_at":"2026-04-06T16:43:43.743509570Z","created_by":"coding","updated_at":"2026-04-07T15:52:40.588868807Z","closed_at":"2026-04-07T15:52:40.588753306Z","close_reason":"Implemented the full 10-step ordered shutdown sequence so the mothership drains cleanly without data loss on SIGTERM (Docker stop, Kubernetes termination).\n\nShutdown sequence (30s hard deadline):\n1. Set shutting_down=true; ingestion server returns HTTP 503 to new WebSocket upgrade requests\n2. Broadcast {type:'shutdown', reconnect_in_ms:30000} to all dashboard WebSocket clients\n3. Cancel fusion loop context (stops fusion goroutine)\n4. Drain signal processing pipeline: wait for in-flight CSI frames (max 2s)\n5. Flush in-memory baselines to SQLite in a single transaction\n6. Sync CSI recording buffer to disk (close writer, fsync)\n7. Close all node WebSocket connections with normal close frame (1000)\n8. Write {type:'system', description:'Mothership stopped'} event to events table\n9. PRAGMA wal_checkpoint(FULL) to collapse WAL into main DB file\n10. sqlite3.Close()\n\nEach step gets its own log line: '[SHUTDOWN] Step N/10 — ...'\nSteps that fail log ERROR but do not abort remaining steps.\nExit code 0 if all steps completed within deadline; exit code 1 if deadline exceeded.","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":"","labels":["failure-count:4"]} -{"id":"spaxel-a1f","title":"Morning briefing","description":"## Background\n\nThe morning briefing is a daily summary delivered when the first household member opens the dashboard (or wakes up, detected by the presence system) after the quiet hours / sleep period. It collects the most relevant information from the previous night and the upcoming day into a single concise card, so the user starts the day informed about their home without any active effort. A good morning briefing is like a trusted home assistant quietly summarising overnight events.\n\n## BriefingGenerator\n\nNew package: mothership/internal/briefing/generator.go\n\nBriefingGenerator runs as a scheduled goroutine, generating a DailyBriefing at a configurable time (default 06:00 server-local time). The briefing is stored in SQLite (daily_briefings table) and pushed to dashboard clients on their first connection after the briefing time.\n\nDailyBriefing struct:\n- id TEXT (UUID)\n- date DATE (the date this briefing covers — typically \"today\", covering the previous night)\n- generated_at DATETIME\n- sections []BriefingSection\n- delivered BOOLEAN (set true after first push to any dashboard client)\n- acknowledged BOOLEAN (set true when user dismisses the card)\n\nEach BriefingSection has a type and content:\n- SectionType: \"sleep\", \"overnight_events\", \"system_health\", \"predictions\", \"weather\" (optional)\n\n## Section: Sleep Report\n\nFor each person with a completed sleep session from the previous night:\nGenerated from the sleep_sessions table (Phase 7, spaxel-qfp).\nContent example: \"Alice slept 7h 23m. 2 wake-ups, 91% efficiency. Avg breathing 14/min.\"\nIf no sleep session: \"No sleep data for Alice last night.\"\nIf sleep session is still in progress (woke up after 6am): \"Alice is still asleep.\"\n\nFormatting rules:\n- Good sleep (efficiency > 85%, duration > 7h): green indicator\n- Fair sleep (efficiency 70-85% or duration 6-7h): amber indicator\n- Poor sleep (efficiency < 70% or duration < 6h): red indicator\n- Anomaly (breathing rate anomaly flagged): include note: \"Unusual breathing pattern detected at 02:14.\"\n\n## Section: Overnight Events\n\nSummary of activity timeline events that occurred during the quiet hours period (e.g. 10pm-6am).\nFilter: FallDetected, AnomalyDetected, NodeDisconnected events only.\nIf no events: \"No incidents overnight.\" (Reassuring — users should see this most mornings.)\nIf events: \"Node Living Room went offline at 02:15 and reconnected at 02:47.\" or \"Anomaly detected at 03:30 in Kitchen (acknowledged).\"\nFall events: always prominently listed, even if acknowledged: \"Possible fall detected at 04:12 for Alice in Bedroom (acknowledged).\"\nLimit: maximum 5 events summarised. If more than 5: \"...and 3 more events. [View all]\"\n\n## Section: System Health\n\nQuick summary of current system health.\nContent: \"4 nodes healthy.\" or \"3 nodes healthy. Node Hallway has been offline since 02:15.\"\nIf a node has been offline > 1 hour: include the duration.\nLink health average: \"Detection quality: 92%.\" (from ambient confidence score, Phase 5).\n\n## Section: Predictions\n\nFor each tracked person, their predicted first activity today.\nGenerated from the presence prediction model (Phase 7, spaxel-hnp).\nContent: \"Alice typically leaves at 8:30am on Tuesdays (78% confidence).\"\nOnly included if prediction model has sufficient data (> 7 days per person).\nIf prediction confidence < 60%: omit (not useful at low confidence).\n\n## Section: Weather (Optional)\n\nIf a weather API URL is configured in settings (e.g. OpenWeatherMap, wttr.in):\nFetch current outdoor temperature and conditions.\nContent: \"Outside: 14°C, partly cloudy.\"\nThis is a nice-to-have context note for heating decisions. The API call is optional and fails gracefully (omit the section if the API is unavailable).\n\nImplementation: GET https://wttr.in/{location}?format=%t+%C (plain text format). Cache for 30 minutes.\n\n## Dashboard Delivery\n\nOn the first WebSocket connection after 06:00 from any dashboard client:\n1. Check if today's DailyBriefing exists and has not been delivered yet\n2. If yes: push {\"type\":\"morning_briefing\",\"briefing\":BriefingJSON} to that client\n3. Mark briefing.delivered = true\n\nDashboard rendering:\n- Simple mode: full-width dismissible card at the very top of the home view. Large enough to read at a glance. Dismiss button (X) in top-right corner.\n- Expert mode: a floating panel overlay (300px wide, top-right of screen), auto-dismisses after 30 seconds of inactivity (the user sees it then goes back to work)\n- Ambient mode: the morning briefing overlay already handled by the ambient mode bead (spaxel-5es) — this briefing generator feeds that overlay.\n\nAcknowledgement: the dismiss button sends POST /api/briefings/{id}/acknowledge. Sets briefing.acknowledged = true.\n\n## Push Notification Delivery\n\nThe briefing is also pushed as a notification at 06:00 (via the notification module, Phase 6 spaxel-zpt), even if no dashboard is open. This ensures users get their morning summary even if they have not opened the dashboard.\n\nThe notification includes a floor-plan thumbnail (from the floor-plan renderer, spaxel-zpt) showing the current home state at 06:00.\n\nNotification title: \"Good morning\" (or \"Good morning, Alice\" if single person)\nNotification body: condensed version of the briefing: the most important item from each section.\n\n## Files to Create or Modify\n\n- mothership/internal/briefing/generator.go: BriefingGenerator, DailyBriefing, section generation\n- mothership/internal/briefing/scheduler.go: daily schedule (6am goroutine)\n- mothership/internal/dashboard/hub.go: morning_briefing push on first connection\n- dashboard/js/briefing.js: morning briefing card rendering in simple and expert modes\n- mothership/internal/dashboard/routes.go: POST /api/briefings/{id}/acknowledge, GET /api/briefings/today\n\n## Tests\n\n- Test briefing generation at 06:00: mock time at 06:00, verify BriefingGenerator creates a DailyBriefing\n- Test sleep section: inject a completed sleep session, verify section content is correct\n- Test overnight events: inject a NodeDisconnected event at 03:00, verify it appears in the overnight section\n- Test system health section: inject node_offline state, verify it appears correctly with duration\n- Test that briefing.delivered is set to true after the first push\n- Test that the briefing is pushed only once (second connection after delivery does not re-push)\n- Test push notification is sent at 06:00 (with mock time and mock notification module)\n\n## Acceptance Criteria\n\n- Morning briefing card appears on the first dashboard open after 06:00 (configurable)\n- All sections (sleep, overnight events, system health, predictions) are correctly populated\n- Briefing is pushed only once per day (not re-pushed on second dashboard open)\n- Push notification delivered at 06:00 with condensed briefing summary\n- Dismiss button correctly acknowledges the briefing and hides the card\n- \"No incidents overnight\" message appears correctly on quiet nights\n- Weather section appears when weather API is configured\n- Tests pass","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":3,"issue_type":"task","assignee":"bravo","owner":"","created_at":"2026-03-28T02:03:19.830232092Z","created_by":"coding","updated_at":"2026-04-11T08:06:51.045338042Z","closed_at":"2026-04-11T08:06:51.045230298Z","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:17"],"dependencies":[{"issue_id":"spaxel-a1f","depends_on_id":"spaxel-qfp","type":"blocks","created_at":"2026-03-28T02:03:23.803412940Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-a1f","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:15.060252766Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-a55","title":"Dashboard: anomaly detection & security mode UI","description":"## Overview\n\nThe anomaly detection backend (mothership/analytics/ or signal/) is ~80% complete. This bead covers the remaining backend wiring and the full dashboard UI for security mode.\n\n## Backend (if not yet done)\n- Confirm AnomalyDetector is initialized and running in main()\n- Anomaly events must be pushed to the dashboard WS feed as 'alert' messages (requires spaxel-9eg)\n- GET /api/anomalies?since=24h — list recent anomaly events\n- POST /api/security/arm + /api/security/disarm — arm/disarm security mode\n- GET /api/security/status — { armed, learning_until, anomaly_count_24h }\n\n## Dashboard UI (dashboard/js/security-panel.js)\n\n### Security mode card (always visible in header or sidebar)\n- Arm / Disarm toggle button with confirmation dialog\n- Status badge: DISARMED / LEARNING (N days remaining) / ARMED / ALERT\n- Learning period progress bar: '5 of 7 days complete'\n- Last anomaly: '2 hours ago — kitchen motion at 3:14am'\n\n### Alert banner\n- Full-width red banner when anomaly triggered while armed\n- Description, timestamp, affected zone\n- Acknowledge button (POST /api/anomalies/{id}/acknowledge)\n\n### Anomaly timeline tab\n- List of recent anomaly events with severity, zone, timestamp\n- Links to timeline view for full context\n\n## Acceptance\n\n- Arm/disarm persists across server restarts\n- Learning period progress updates on page refresh\n- Anomaly alert banner appears within 2s of detection\n- Acknowledged alerts disappear from the banner (not from history)\n\n## Note\n\nCloses or supersedes spaxel-403 if that bead's remaining work matches this scope.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"romeo","owner":"","created_at":"2026-04-06T12:56:14.953369134Z","created_by":"coding","updated_at":"2026-04-25T13:04:10.210239764Z","closed_at":"2026-04-25T13:04:10.210171225Z","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:338"],"dependencies":[{"issue_id":"spaxel-a55","depends_on_id":"spaxel-7x2","type":"blocks","created_at":"2026-04-06T16:09:35.835740127Z","created_by":"coding","thread_id":""},{"issue_id":"spaxel-a55","depends_on_id":"spaxel-d04","type":"blocks","created_at":"2026-04-06T16:09:35.892072969Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-axa","title":"Phase 4: Onboarding & OTA","description":"Goal: Non-technical users can add and update nodes. Interactive guided wizard.\n\nDeliverables:\n- Interactive onboarding wizard (flash via Web Serial, guided walk-through with live sensor feedback)\n- Provisioning payload (WiFi creds + node ID config blob → NVS)\n- OTA system (HTTP firmware serving, WebSocket-triggered, rolling updates, automatic rollback)\n- Captive portal recovery (AP fallback, config page for re-provisioning)\n- Guided troubleshooting foundation (first-time tooltips, node-offline steps, reinforcement)\n\nExit criteria: New ESP32-S3 from unboxed to streaming CSI in under 5 minutes.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"phase","assignee":"echo","owner":"","created_at":"2026-03-27T01:55:16.517644233Z","created_by":"coding","updated_at":"2026-03-29T03:57:13.016777393Z","closed_at":"2026-03-29T03:57:13.016673982Z","close_reason":"Phase 4: Onboarding & OTA System - COMPLETE\n\nAll deliverables implemented and committed (90e230f):\n\nInteractive Onboarding Wizard:\n- 8-step Web Serial-based provisioning flow\n- Firmware flashing via esp-web-install-button (CDN)\n- Live CSI waveform feedback during guided calibration\n- Server-side provisioning with client-side fallback\n\nOTA Firmware Management:\n- Firmware list with SHA-256 hashes and size display\n- Per-node progress tracking\n- Rolling update orchestration via REST API\n- Status bar button with state indicators\n\nGuided Troubleshooting:\n- First-time feature tooltips with 8s auto-dismiss\n- Sequential tooltip tour on first node connection\n- Node offline cards with recovery instructions\n- Client-side link health check (60s threshold)\n\nExit criteria met: New ESP32-S3 from unboxed to streaming CSI in under 5 minutes. 96 tests passing.","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-axa","depends_on_id":"spaxel-uc9","type":"blocks","created_at":"2026-03-28T01:33:40.376191069Z","created_by":"coding","thread_id":""}]} -{"id":"spaxel-b49t","title":"fleet: surface Unpaired nodes in /api/fleet/health response","description":"## What\nMerge unpaired (connected-but-unprovisioned) nodes into the fleet health response so the dashboard can show them.\n\n## Changes\n- Add `UnpairedProvider` interface (`GetUnpairedMACs() []string`) to `fleet/fleethandler.go`\n- Add `unpairedProvider` field + `SetUnpairedProvider` setter to `FleetHandler`\n- Add `Unpaired bool` to `fleetNodeEntry`\n- In `getFleetHealth` and `getFleet`: for each unpaired MAC not already in the registry, append an entry with `Unpaired: true`; for MACs in the registry, set `Unpaired: true` on their entry\n\n## File\n`mothership/internal/fleet/fleethandler.go`","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","owner":"","created_at":"2026-04-24T22:35:26.466438566Z","created_by":"coding","updated_at":"2026-04-24T22:46:13.299704914Z","closed_at":"2026-04-24T22:46:13.299644958Z","close_reason":"done","closed_by_session":"","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"original_size":0,"sender":""} -{"id":"spaxel-b6a","title":"Implement calibration POST endpoint","description":"## Task\nImplement POST /api/floorplan/calibrate endpoint.\n\n## Specification\n- Accept {ax,ay,bx,by,distance_m,rotation_deg}: two pixel coordinates and their real-world distance\n- Compute and persist pixel-to-meter transform to SQLite floorplan table\n\n## Acceptance\n- Calibration data persists to SQLite floorplan table","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","owner":"","created_at":"2026-04-07T17:55:52.516620827Z","created_by":"coding","updated_at":"2026-04-07T18:53:06.721066Z","closed_at":"2026-04-07T18:53:06.720893263Z","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-klk"]} -{"id":"spaxel-bf5","title":"Build crowd flow visualization","description":"Create visualization tools for occupancy patterns and movement.\n\nDeliverables:\n- Trajectory accumulation over time\n- Directional flow map rendering\n- Dwell time hotspot visualization\n\nAcceptance: Dashboard shows accumulated movement patterns and hotspots.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"sp3","owner":"","created_at":"2026-03-29T19:25:04.155117811Z","created_by":"coding","updated_at":"2026-03-29T19:39:12.378329742Z","closed_at":"2026-03-29T19:39:12.378228906Z","close_reason":"Implemented crowd flow visualization with three components:\n\nBackend (Go):\n- FlowAccumulator records trajectory segments and dwell time in SQLite\n- REST endpoints for flow map, dwell heatmap, and detected corridors\n- Bresenham rasterization, angular variance analysis, connected component labeling\n\nFrontend (JavaScript):\n- Pattern controls in dashboard sidebar (flows, dwell, corridors toggles)\n- Time filter dropdown (7d, 30d, all time)\n- 3D visualization with ArrowHelper, PlaneGeometry, pulsating animations\n\nFiles: dashboard/index.html, dashboard/js/app.js, mothership/internal/analytics/","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-i28"]} -{"id":"spaxel-bnbj","title":"Fix simple.html blank page (ES module loaded as classic script)","description":"## Problem\n\n`simple.html` renders completely blank. Browser console shows:\n\n```\nUnexpected token 'export'\n```\n\nAn ES-module script is being loaded with a classic `