diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 33e71ec..92c56df 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -41,7 +41,7 @@ {"id":"spaxel-iq3","title":"Biomechanical blob tracking with UKF","description":"## Background\n\nRaw blob detections from FusionEngine (spaxel-FUSION) are noisy — blobs appear/disappear and jump between frames. We need a filter that: persists tracks through 5-frame occlusion gaps, constrains motion to physically plausible human movement, enables identity assignment (person A vs person B), and infers posture hints for 3D visualization. The Unscented Kalman Filter (UKF) is used because the motion model is nonlinear and UKF avoids requiring a Jacobian.\n\n## What to Implement\n\nNew package: mothership/internal/tracker/\n\n### UKF State\nState vector: [x, y, z, vx, vy, vz] (6-DOF: position + velocity)\n\nProcess model: constant velocity with acceleration noise. Process noise covariance Q:\n- Position noise: sigma_pos = 0.05m (small — position doesn't jump)\n- Velocity noise: sigma_vel = 0.5 m/s per axis (humans can accelerate ~3 m/s²)\n- Z velocity noise: sigma_vz = 0.05 m/s (humans rarely move vertically fast)\n\nHuman constraints applied as post-update clamps:\n- Max speed: clamp ||v|| to 2.0 m/s\n- Z position: clamp to [0.0, 2.5m] (floor to ceiling)\n- Z velocity: clamp to [-0.5, 0.5] m/s during normal locomotion (0.2m/s for sit-down)\n\n### TrackManager\n- mothership/internal/tracker/manager.go\n- Update(blobs []fusion.BlobDetection, timestamp time.Time) []TrackState\n- Data association: Hungarian algorithm for ≤6 blobs (O(n³)), greedy nearest-neighbor for >6\n- Mahalanobis distance gating: reject assignments if distance > chi2_threshold(0.99, df=3) ≈ 11.34\n- Track lifecycle:\n - TENTATIVE: blob seen 1-2 times. Not reported in output.\n - CONFIRMED: seen ≥3 consecutive updates. Reported.\n - COASTED: no detection for 1-5 updates. UKF predict-only. Still reported.\n - DELETED: no detection for >5 updates. Removed from state.\n- Collision avoidance: if two CONFIRMED tracks within 0.5m, add a repulsion force to the weaker track\n\n### Posture inference (heuristic, no ML)\nFrom TrackState position and velocity:\n- WALKING: speed > 0.3 m/s\n- STANDING: speed < 0.1 m/s, z > 1.0m\n- SEATED: speed < 0.1 m/s, 0.3m < z < 0.8m \n- LYING: z < 0.3m (regardless of speed)\n\n### Output\nTrackState: {id string, position vec3, velocity vec3, posture Posture, confidence float32, age int (frames)}\nBroadcast via hub as 'track_update' JSON at 10Hz, same cadence as blob_update.\n\n### Integration\nFusionEngine calls TrackManager.Update() after each BlobExtractor run.\n\n## Key Files\n- mothership/internal/fusion/engine.go — call TrackManager.Update() after blob extraction\n- mothership/internal/fusion/blobs.go — BlobDetection type\n- New: mothership/internal/tracker/ukf.go, tracker/manager.go, tracker/association.go + tests\n\n## Acceptance Criteria\n- Track IDs stable across 5-frame gaps\n- Two tracks do not merge when blobs cross within 0.5m\n- Posture WALKING fires when speed > 0.3 m/s\n- TENTATIVE tracks not included in track_update output\n- Hungarian assignment correct for 4-blob test case\n- go test ./internal/tracker/... passes","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-28T03:30:50.410975161Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.222736024Z","closed_at":"2026-03-28T05:36:26.222463200Z","close_reason":"Implemented: tracker/tracker.go + tracker/ukf.go (59404aa) — 6-DOF UKF, human motion constraints, TrackManager Hungarian association, Mahalanobis gating, posture inference (walking/standing/seated/lying)","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"spaxel-iq3","depends_on_id":"spaxel-6th","type":"blocks","created_at":"2026-03-28T03:30:50.410975161Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-iv3","title":"Dashboard: detection explainability overlay","description":"## Overview\n\nUsers need to understand why a blob is detected where it is. The 'Why is this here?' overlay is the primary explainability tool and a key trust-building feature.\n\n## What to build\n\n### Trigger\n- Right-click (or long-press) on a blob in the 3D view → context menu includes 'Why is this here?'\n- Also accessible from the blob's info panel\n\n### Overlay (dashboard/js/explainability.js)\n- Dim all scene elements except contributing links and the selected blob (THREE.js material opacity)\n- Highlight contributing links with a glowing yellow shader (MeshLineMaterial or emissive)\n- Render Fresnel zone ellipsoids for each contributing link at reduced opacity\n- Show intersection zones where Fresnel ellipsoids overlap (this is the detection hotspot)\n\n### Sidebar panel\n- Title: 'Detection confidence: 87%'\n- Per-link contribution table:\n | Link | deltaRMS | Zone # | Weight | Contributing? |\n |------|----------|--------|--------|---------------|\n | A:B | 0.041 | 1 | 0.34 | ✓ |\n- BLE match section (if applicable): 'Matched: Alice's iPhone (96% confidence)'\n- 'Close' button restores normal scene\n\n### Data source\n- GET /api/explain/{blob_id} — returns per-link contributions, confidence breakdown, BLE match\n- Must be implemented server-side (mothership/internal/fusion or localization)\n\n## Acceptance\n\n- Overlay renders within 300ms of user action\n- Non-contributing links are visually distinct from contributing ones\n- Fresnel ellipsoids visible and correctly scaled\n- Close button fully restores scene state","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-06T12:56:27.780919021Z","created_by":"coding","updated_at":"2026-04-06T12:56:27.780919021Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"spaxel-jc4","title":"Self-healing fleet with role re-optimisation","description":"## Background\n\nIn a home deployment, nodes experience disruptions: power outages causing reboots, firmware crashes, being physically moved by a curious family member, or being temporarily unplugged. The fleet manager (spaxel-8u3) handles basic node connection management. This bead adds intelligence: when the set of available nodes changes, the system recomputes the optimal role assignment — which node should be TX, which should be RX, which should be passive radar — to maximise coverage over the occupied space.\n\nThis is \"self-healing\" in the sense that the system automatically recovers from topology changes without user intervention, maintaining the best possible detection coverage with whatever nodes are currently available.\n\n## Role Assignment Background\n\nA spaxel node can operate in one of four roles:\n- RX: passive receiver, listens for CSI from other nodes\n- TX: active transmitter, sends at configured rate\n- TX-RX: both transmits and receives (most flexible but uses more power)\n- Passive: uses existing WiFi traffic as CSI source (no active transmission)\n\nA sensing link requires one TX and one RX. With N nodes, the number of possible links is bounded by the assignment. Orthogonal links (non-collinear Fresnel zones) provide better coverage than parallel links. The optimal assignment maximises the number of orthogonally-placed links covering the occupied zones.\n\n## RoleOptimiser\n\nNew struct RoleOptimiser in mothership/internal/fleet/optimiser.go.\n\nInputs:\n- NodeList: current connected nodes with their 3D positions (from placement UI, spaxel-qq6)\n- NodeCapabilities: map from MAC to {canTX bool, canRX bool, hardwareType string} (from hello message capabilities field)\n- LinkHealthScores: current health scores from ambient confidence bead (spaxel-sbi)\n- RoomConfig: room dimensions and zone occupancy from config\n\nOutput:\n- RoleAssignment: map from MAC to Role\n\nOptimisation algorithm (greedy, O(n^2) which is fine for n <= 16 nodes):\n1. Start with all nodes unassigned\n2. For each pair of nodes (A, B) that both have adequate health and are within sensing range:\n - Compute the angle between their TX-RX axis and all other assigned TX-RX axes\n - Score = sum of min angular separation to each existing assigned link axis (penalise near-parallel links)\n3. Assign the pair with the highest score as TX-RX\n4. Repeat for remaining unassigned nodes until all capable nodes are assigned\n5. For nodes that cannot improve coverage (e.g. co-located with an existing node), assign as passive\n\nThe GDOP computation from Phase 3 (spaxel-qq6 coverage painting) provides a more principled score: compute GDOP for the candidate assignment and maximise the average GDOP over occupied zones. If the GDOP computation is available, use it; otherwise fall back to the angular separation heuristic.\n\n## Graceful Degradation on Node Loss\n\nWhen a node disconnects (WebSocket closes), the fleet manager immediately:\n\n1. Marks the node as OFFLINE in the node registry\n2. Identifies which sensing links have been lost (links where the offline node was TX or RX)\n3. Calls RoleOptimiser.Optimise(currentAvailableNodes) to get the new optimal assignment\n4. Compares old GDOP to new GDOP:\n - If new GDOP >= old GDOP * 0.9 (no significant coverage degradation): apply new assignment silently\n - If new GDOP < old GDOP * 0.9 (significant coverage degradation): apply new assignment AND show dashboard warning\n5. Broadcasts RoleChange commands to all affected surviving nodes via WebSocket\n6. Broadcasts fleet_change event to dashboard with before/after GDOP overlay data\n\nDashboard warning when coverage is significantly degraded:\n\"Detection accuracy reduced — Node [label] is offline. [Zone name] coverage dropped from [N]% to [M]%. [View impact] [Dismiss]\"\nThe \"View impact\" button shows a side-by-side 3D GDOP overlay comparison.\n\n## 5-Minute Reconnect Window\n\nIf the offline node reconnects within 5 minutes of going offline:\n- Do NOT run the optimiser again\n- Restore the node's previous role assignment from the node registry\n- Send the role push command to the reconnected node\n- Clear the coverage reduction warning in the dashboard\n- Log: \"Node [label] reconnected — restoring previous role\"\n\nThis prevents unnecessary churn when nodes experience brief power blips or firmware restarts. The 5-minute window is configurable via mothership config (fleet.reconnect_grace_period_seconds, default 300).\n\n## Before/After Coverage Comparison\n\nWhen re-optimisation occurs, compute GDOP for the old and new assignments and store both in the fleet_change event:\n- gdop_before: 2D array of GDOP values over the floor plan grid with old assignment\n- gdop_after: 2D array with new assignment\n- coverage_change_pct: percentage change in occupied-zone coverage\n\nThe dashboard 3D view can render these as two overlaid heat maps (before in red, after in green, with a blend slider).\n\n## Dashboard Fleet Health Panel\n\nAdd a \"Fleet Health\" section to the dashboard (sidebar panel or dedicated route):\n- Current role assignment: table showing each node, its role, and health score\n- Coverage quality: Detection Quality gauge (from ambient confidence bead) and GDOP overlay toggle\n- Re-optimisation history: last 5 optimisation events with timestamps, trigger reason, and GDOP change\n- \"Optimise Now\" button: manually triggers the optimiser regardless of coverage change threshold\n- \"Simulate node removal\" tool: shows predicted coverage impact if a specific node were removed (useful for planning maintenance)\n\n## Files to Create or Modify\n\n- mothership/internal/fleet/optimiser.go: RoleOptimiser struct and Optimise() method\n- mothership/internal/fleet/manager.go: extend with reconnect grace period, degradation detection\n- mothership/internal/fleet/manager.go: add fleet_change event emission\n- dashboard/js/fleet.js: fleet health panel, before/after GDOP comparison view\n- mothership/internal/dashboard/routes.go: GET /api/fleet/history, POST /api/fleet/optimise\n\n## Tests\n\n- Test that role optimiser selects the most orthogonal link pair from a set of 4 nodes at known positions\n- Test graceful degradation: when a node goes offline, the optimiser produces a valid assignment for the remaining nodes\n- Test 5-minute reconnect window: reconnecting node within 300s restores previous role without re-optimisation\n- Test that the reconnect window expires correctly at 300s and the optimised assignment is kept\n- Test GDOP comparison logic: when new GDOP >= old * 0.9, no dashboard warning; when < 0.9, warning fires\n- Test fleet_change event contains correct before/after GDOP data\n\n## Acceptance Criteria\n\n- Node loss triggers role re-optimisation within 10 seconds\n- Dashboard shows coverage impact for significant degradation (>10% GDOP change)\n- Reconnecting nodes within 5 minutes restore their previous role without unnecessary re-optimisation\n- Re-optimisation does not disrupt surviving links (surviving nodes receive new role commands without dropouts)\n- Coverage comparison overlay visible in dashboard when re-optimisation is triggered\n- \"Optimise Now\" manual trigger works\n- Tests pass","status":"in_progress","priority":3,"issue_type":"task","assignee":"charlie","created_at":"2026-03-28T01:42:17.825002481Z","created_by":"coding","updated_at":"2026-04-02T01:18:14.868719476Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","failure-count:921"]} -{"id":"spaxel-jcc","title":"Reintegrate phase 6+ packages into default build","description":"## Problem\n\nmain_phase6.go uses a 'phase6' build tag which excludes all Phase 6+ code from the default binary. All backend work from Phase 6 onward is dead code that never runs.\n\n## What needs to change\n\n- Remove the 'phase6' build tag gating from main_phase6.go (or merge into cmd/mothership/main.go)\n- Ensure all packages under: automation/, events/, notify/, replay/, prediction/, learning/, analytics/, sleep/, tracker/, zones/, mqtt/ are wired into the server at startup\n- Run 'go build ./...' to confirm all packages compile cleanly\n- Run 'go test ./...' — all tests must pass\n\n## Files to check\n\n- mothership/cmd/mothership/main.go\n- mothership/cmd/mothership/main_phase6.go (if it exists)\n- Any file with '//go:build phase6'\n\n## Acceptance\n\n'go build -o /dev/null ./...' succeeds with no build tags. All routes, goroutines, and managers from phase 6 packages are initialized in main().","status":"in_progress","priority":1,"issue_type":"task","assignee":"alpha","created_at":"2026-04-06T12:55:18.201589244Z","created_by":"coding","updated_at":"2026-04-06T13:51:31.110911913Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["failure-count:17"]} +{"id":"spaxel-jcc","title":"Reintegrate phase 6+ packages into default build","description":"## Problem\n\nmain_phase6.go uses a 'phase6' build tag which excludes all Phase 6+ code from the default binary. All backend work from Phase 6 onward is dead code that never runs.\n\n## What needs to change\n\n- Remove the 'phase6' build tag gating from main_phase6.go (or merge into cmd/mothership/main.go)\n- Ensure all packages under: automation/, events/, notify/, replay/, prediction/, learning/, analytics/, sleep/, tracker/, zones/, mqtt/ are wired into the server at startup\n- Run 'go build ./...' to confirm all packages compile cleanly\n- Run 'go test ./...' — all tests must pass\n\n## Files to check\n\n- mothership/cmd/mothership/main.go\n- mothership/cmd/mothership/main_phase6.go (if it exists)\n- Any file with '//go:build phase6'\n\n## Acceptance\n\n'go build -o /dev/null ./...' succeeds with no build tags. All routes, goroutines, and managers from phase 6 packages are initialized in main().","status":"in_progress","priority":1,"issue_type":"task","assignee":"alpha","created_at":"2026-04-06T12:55:18.201589244Z","created_by":"coding","updated_at":"2026-04-06T14:03:39.282294536Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["failure-count:21"]} {"id":"spaxel-jk0","title":"Simple mode with progressive disclosure","description":"## Background\n\nThe 3D expert interface is powerful but overwhelming for casual household members who just want to know \"is anyone home?\" or \"is the baby still asleep?\". Simple mode is a card-based, mobile-first UI that surfaces the most important information without any 3D scene. It should be usable by anyone who can read a smartphone screen, including elderly family members and non-technical partners who did not install the system.\n\nProgressive disclosure means: start with the simplest possible view, and let users reach more complexity when they want it — without being exposed to complexity they don't need.\n\n## Auto-Detection of Simple Mode\n\nSimple mode is automatically selected as the default view based on:\n1. Screen width < 768px (phones, small tablets in portrait)\n2. User-agent contains \"Mobile\" (additional phone detection signal)\n3. User has previously selected simple mode (localStorage \"spaxel_mode\" = \"simple\")\n\nExpert mode is the default for desktop browsers. A user can override the auto-detection and save their preference.\n\n## Room Occupancy Cards\n\nThe main view is a card grid (CSS Grid, 1 column on phones, 2 columns on small tablets). One card per defined zone.\n\nEach occupancy card shows:\n- Zone name (large, readable typography, min 20px font size)\n- Zone colour as a left border accent\n- Occupant count: large number\n- Named occupants: first names in a row (e.g. \"Alice, Bob\"). Anonymous tracks: \"1 person\"\n- Status icon: person silhouette if occupied, empty room icon if vacant\n- \"Last activity\" time: \"3 minutes ago\" (time since last zone transition event in this zone)\n\nAuto-update: cards update in real-time via WebSocket. When occupancy changes, the card animates briefly (a gentle pulse or highlight) to draw attention to the change.\n\nEmpty state (no zones defined): show a \"Get started\" prompt: \"Set up your rooms to see who's home. [Go to setup]\"\n\n## Activity Feed\n\nScrollable list below the zone cards (or as a separate tab). Shows the last 20 person-relevant events, filtered to exclude system noise (no node_connected, no weight_update events — only ZoneTransition, FallDetected, AnomalyDetected).\n\nEvent items: icon + one-line description + timestamp. Examples:\n- \"Alice walked into the Kitchen — 2 minutes ago\"\n- \"Bob left the house — 14 minutes ago\"\n- \"No one home since 8:32am\"\n- \"Possible fall: Alice in Hallway — 3 minutes ago [View] [Acknowledge]\"\n\nPlain English descriptions — no jargon. \"Possible fall\" not \"FallDetected event in zone_hallway_01.\"\n\nTap any event: shows a brief detail popup (not a full detail view). For zone transitions: \"Alice (via Living Room door) at 14:23\". For falls: the fall alert card with acknowledge button.\n\n## Alert Banner\n\nWhen an active unacknowledged alert exists (fall, anomaly, node offline), show a full-width banner at the top of the simple mode view:\n- Fall alert: red background, \"Possible fall — Alice in Hallway. [Acknowledge]\"\n- Anomaly (away mode): orange background, \"Movement detected while away. [View details]\"\n- Node offline: yellow background, \"Node Living Room went offline. [Help]\"\n\nAlerts are ordered by severity (fall > anomaly > node offline) if multiple are active. The [Help] button links to the troubleshooting flow (spaxel-r0l Phase 4 bead).\n\n## Sleep Summary Card\n\nA morning-only card, shown only between 6am and 11am on the day after a sleep session:\n- Position: above zone cards (highest priority in morning)\n- Content: \"Alice slept 7h 23m last night. 2 brief wake-ups. [View details]\"\n- If multiple people: stacked cards or a compact multi-person summary\n- Dismiss button: card hidden for today (localStorage flag \"spaxel_sleep_summary_{date}_shown\")\n- \"View details\" navigates to the Sleep panel in expert mode\n\n## Navigation\n\nBottom navigation bar (mobile-standard pattern): five tabs with icons + labels:\n1. Home (house icon) — occupancy cards (default tab)\n2. Activity (clock icon) — activity feed\n3. (empty centre spot for potential quick-action FAB in future)\n4. Alerts (bell icon) — active alerts and history. Badge with alert count.\n5. Settings (gear icon) — simplified settings: notification channel, person names, mode toggle\n\nThe Settings tab in simple mode shows only: display name for each person (from BLE registry), notification channel status (green = configured, grey = not set), and \"Switch to expert mode\" at the bottom.\n\n## Expert Mode Toggle\n\nA clearly labelled button: \"Expert Mode\" in the settings tab and as a persistent bottom-nav item. Tapping it:\n- If a PIN is configured (expert mode lock, settable in expert mode settings): shows PIN entry pad\n- If no PIN: immediately switches to expert mode\n- Saves preference to localStorage\n\nThe mode toggle is intentionally in settings/bottom-nav rather than prominently on the home screen — to reduce accidental switches for non-technical users.\n\n## Night Mode (OLED Dark)\n\nAuto-active during configured quiet hours (e.g. 10pm-7am). Uses CSS media query prefers-color-scheme: dark plus a manual override. OLED optimised: true black background (#000000), not just dark grey. This saves battery on OLED screens and reduces light disruption in bedrooms.\n\nAll card backgrounds in night mode: #0a0a0a (near-black). Text: #ffffff. Zone colour accents remain colourful.\n\n## Design System\n\nSimple mode uses a separate CSS file (dashboard/css/simple.css) that does NOT import from the expert mode styles. No Three.js canvas, no OrbitControls, no shader materials. Pure HTML + CSS + vanilla JS.\n\nFont size hierarchy: 14px minimum for secondary text, 16px for primary, 24px+ for zone names and counts. All interactive targets: minimum 44px height for WCAG AA touch target compliance.\n\n## Files to Create or Modify\n\n- dashboard/simple.html: simple mode HTML shell with bottom nav\n- dashboard/js/simple.js: card rendering, WebSocket updates, event feed\n- dashboard/js/simplemode.js: mode detection, localStorage mode preference\n- dashboard/css/simple.css: simple mode styles, night mode\n- mothership/internal/dashboard/routes.go: ensure /simple route is served\n\n## Tests\n\n- Test that room occupancy cards correctly reflect current zone state from WebSocket messages\n- Test activity feed filtering: inject a node_connected event and a zone_transition event; only the zone_transition should appear in simple mode feed\n- Test alert banner appears and dismisses correctly on acknowledge\n- Test mode toggle correctly switches between simple and expert mode routes\n- Test sleep summary card appears only between 6am-11am on the morning after a session\n- Test that occupancy card updates show the animated pulse on change\n- Test night mode activates based on quiet hours configuration\n\n## Acceptance Criteria\n\n- Simple mode loads correctly on a 320px wide screen (iPhone SE) without horizontal scrolling\n- Occupancy cards update in real-time as people move between zones\n- Activity feed shows only person-relevant events in plain English\n- Alert banner appears prominently and dismisses on acknowledge\n- Sleep summary card shown between 6am-11am after sleep session, dismissed when tapped\n- Mode toggle to expert mode works and saves preference\n- Night mode activates during configured quiet hours\n- All tap targets are at least 44px in height\n- Tests pass","status":"open","priority":3,"issue_type":"task","created_at":"2026-03-28T01:59:32.690434334Z","created_by":"coding","updated_at":"2026-03-28T03:29:14.851752276Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"spaxel-jk0","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:14.851714754Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-jy4","title":"Crowd flow visualisation","description":"## Background\n\nOver days and weeks, the movement patterns of household members accumulate into meaningful flows: the main corridor between bedroom and bathroom, the typical path from the front door to the kitchen, habitual dwell spots (the favourite chair, the home office desk, the kitchen counter). Visualising these as directional flow maps and dwell hotspot heatmaps provides useful insight into how the space is actually used — and can inform furniture placement, automation placement, and even architectural decisions. It's also a compelling visual that demonstrates the system's accumulated knowledge.\n\n## FlowAccumulator\n\nNew package: mothership/internal/analytics/flow.go\n\nFlowAccumulator subscribes to TrackManager updates (10 Hz) and accumulates trajectory data.\n\nTrajectory sampling: for each track update, if the track has moved > 0.2m since the last recorded waypoint (for that track), record the movement:\n- from_xyz: last waypoint position\n- to_xyz: current position\n- speed: metres per second at this step\n- person_id: if identity is known\n- timestamp\n\nThis 0.2m threshold prevents accumulating thousands of micro-samples for stationary people.\n\nSQLite table: trajectory_segments (id TEXT PRIMARY KEY, person_id TEXT, from_x REAL, from_y REAL, from_z REAL, to_x REAL, to_y REAL, to_z REAL, speed REAL, timestamp DATETIME). Only store ground plane (from_z and to_z floor-projected: set to 0 for the flow map, since we render on the ground plane).\n\nTable growth management: the table accumulates indefinitely. Prune segments older than 90 days (configurable) with a daily background job. With 4 people at typical home movement rates, 90 days generates approximately 50,000 segments — manageable for SQLite.\n\n## Flow Map Computation\n\nQuery: for each 0.25m grid cell (same resolution as OccupancyGrid in FusionEngine), average the movement vectors of all trajectory segments that pass through that cell.\n\nSQL approach: for each segment, determine which grid cells it passes through (Bresenham's line algorithm on the grid). Accumulate vector components (to_x - from_x, to_y - from_y) into per-cell accumulators.\n\nIn practice: compute on demand when requested (not continuously). Cache the result for up to 5 minutes (or until a \"flow dirty\" flag is set by new trajectory data).\n\nOutput: FlowMap struct with per-cell vectors (x_component, y_component) and a cell count. Serialised to JSON for the dashboard.\n\n## Dwell Hotspot Heatmap\n\nQuery: for each track update where speed < 0.1 m/s (stationary or near-stationary), increment the dwell counter for the corresponding 0.25m grid cell.\n\nSQLite table: dwell_accumulator (grid_x INT, grid_y INT, person_id TEXT, count INT, last_updated DATETIME, PRIMARY KEY (grid_x, grid_y, person_id)). Aggregated at the person+cell level for person-filtered views.\n\nOutput: DwellHeatmap struct mapping (grid_x, grid_y) to count. Normalised to [0, 1] by dividing by the max count across all cells.\n\n## Corridor Detection\n\nIdentify grid cells with consistently high flow volume AND low angular variance in their flow vectors. These are likely corridors or pathways.\n\nAlgorithm:\n1. For each cell, compute the circular variance of the flow vector angles across all segments that contributed. Low variance = directional consistency = corridor.\n2. Threshold: cells with segment_count > 10 AND circular_variance < 0.3 are candidate corridor cells.\n3. Connected component analysis: group adjacent corridor cells into corridor regions.\n4. Each corridor region is represented by its dominant direction and a bounding box.\n\nCorridor regions are stored in SQLite: detected_corridors (id, centroid_xyz, dominant_direction_xy, length_m, width_m, cell_count, last_computed). Recomputed weekly.\n\n## Time and Person Filters\n\nThe dashboard allows filtering flow data by:\n- Time range: \"Today\", \"This week\", \"This month\", custom date range. Implemented as SQL WHERE timestamp >= ? filters on the trajectory_segments table.\n- Person: filter to show only trajectories attributed to a specific person_id (or \"All people\").\n\nFiltered queries are run on-demand with SQL indices on (timestamp, person_id).\n\n## Dashboard Visualisation\n\nAdd two toggle-able layers to the 3D scene (in addition to existing layers):\n\n1. \"Flow\" layer: render flow vectors as animated arrows on the ground plane. Each arrow is positioned at the cell centre, oriented in the cell's average flow direction, and sized proportional to the flow volume (segment count). Use Three.js ArrowHelper for rendering. Animate: cycle the arrow colour from 0% to 100% opacity (flowing effect) on a 2-second loop. Only render cells with > 5 segments.\n\n2. \"Dwell Hotspot\" layer: render a heatmap on the ground plane as coloured rectangle patches (Three.js PlaneGeometry with MeshBasicMaterial, colour mapped from blue (low dwell) through green to red (high dwell)). Opacity 0.4. Only render cells with > 10 dwell samples.\n\n3. Corridor highlighting: detected corridors rendered as slightly raised platform geometry (extruded rectangle, height 0.01m) with a pathway colour (warm grey, opacity 0.3). Toggle-able as sub-option of the \"Flow\" layer.\n\nLayer controls: new \"Patterns\" section in the 3D layer control panel. Three checkboxes: \"Movement flows\", \"Dwell hotspots\", \"Corridors\". Time filter dropdown: \"All time / Last 7 days / Last 30 days\". Person filter dropdown.\n\n## REST API\n\nGET /api/analytics/flow?person_id=&since=&until= — returns FlowMap JSON\nGET /api/analytics/dwell?person_id=&since=&until= — returns DwellHeatmap JSON\nGET /api/analytics/corridors — returns list of DetectedCorridor\n\n## Tests\n\n- Test trajectory sampling: track moves 0.25m -> segment recorded; track moves 0.05m -> no segment\n- Test flow vector averaging: 5 segments all pointing East -> cell vector = (1, 0); 5 East + 5 North -> cell vector ~= (0.5, 0.5)\n- Test dwell accumulation: 100 track updates at speed=0 in cell (5, 7) -> dwell_accumulator[5][7] count = 100\n- Test corridor detection: 20 aligned segments in adjacent cells with angular_variance < 0.3 -> corridor detected\n- Test time-range filtering: insert segments at T-1day and T-8days; query since T-7days -> only T-1day segment returned\n- Test 90-day pruning job removes old segments\n\n## Acceptance Criteria\n\n- Flow layer renders correctly in 3D view with animated arrows for rooms with > 7 days of data\n- Dwell hotspot heatmap visible and renders high-use spots (favourite chair, kitchen counter) correctly\n- Corridor overlay visible with detected high-traffic pathways\n- Time and person filter controls update the rendered layers\n- Layer toggles show/hide each layer cleanly without scene rebuild\n- Tests pass","status":"open","priority":3,"issue_type":"task","created_at":"2026-03-28T01:52:55.852672681Z","created_by":"coding","updated_at":"2026-03-30T16:27:42.718965Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred"]} {"id":"spaxel-klf","title":"Build self-improving localization","description":"Implement localization that learns from ground truth data.\n\nDeliverables:\n- BLE integration as ground truth source\n- Fresnel zone weight refinement algorithm\n- Continuous weight adjustment based on feedback\n\nAcceptance: Localization accuracy improves automatically as BLE ground truth data accumulates.","status":"in_progress","priority":2,"issue_type":"task","assignee":"delta","created_at":"2026-03-29T19:25:03.995110604Z","created_by":"coding","updated_at":"2026-04-02T01:19:06.575645095Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","failure-count:924","mitosis-child","mitosis-depth:1","parent-spaxel-i28"]} diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index e73b163..f66d414 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -118c6b1c9d861650eaf44d27bdc966a0e06af9ea +d0868f2e59e31e572c6791a323b5b13926b9e638