diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b0f8ea3..ec259a2 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -68,7 +68,7 @@ {"id":"spaxel-d41","title":"Create pre-deployment simulator","description":"Build simulator with:\n- Virtual space definition\n- Virtual nodes\n- Synthetic walkers\n- GDOP overlay\n\nAcceptance: Simulator produces realistic synthetic data matching real-world conditions.","status":"closed","priority":2,"issue_type":"task","assignee":"golf","created_at":"2026-04-09T14:54:38.804838310Z","created_by":"coding","updated_at":"2026-04-10T01:31:26.261658691Z","closed_at":"2026-04-10T01:31:26.261329135Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","failure-count:14","mitosis-child","mitosis-depth:1","parent-spaxel-sl2"],"dependencies":[{"issue_id":"spaxel-d41","depends_on_id":"spaxel-cha","type":"blocks","created_at":"2026-04-09T16:11:25.448768498Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-d41","depends_on_id":"spaxel-esn","type":"blocks","created_at":"2026-04-09T16:11:25.532279654Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-d41","depends_on_id":"spaxel-q9d","type":"blocks","created_at":"2026-04-09T16:11:25.573588703Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-d41","depends_on_id":"spaxel-z43","type":"blocks","created_at":"2026-04-09T16:11:25.492872540Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-dbd","title":"Add floor plan dashboard UI","description":"## 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","status":"closed","priority":2,"issue_type":"task","assignee":"golf","created_at":"2026-04-07T14:46:37.333473683Z","created_by":"coding","updated_at":"2026-04-09T12:37:44.104723182Z","closed_at":"2026-04-09T12:37:44.104601579Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","mitosis-child","mitosis-depth:1","parent-spaxel-6hd"]} {"id":"spaxel-duvd","title":"Create morning briefing feature","description":"Daily summary card with push notification option.","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-10T02:03:09.550342367Z","created_by":"coding","updated_at":"2026-04-10T02:03:09.550342367Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} -{"id":"spaxel-eelr","title":"Build guided troubleshooting","description":"Proactive contextual help and post-feedback explanations.","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-10T02:03:09.614829621Z","created_by":"coding","updated_at":"2026-04-10T02:03:09.614829621Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} +{"id":"spaxel-eelr","title":"Build guided troubleshooting","description":"Proactive contextual help and post-feedback explanations.","status":"closed","priority":2,"issue_type":"task","assignee":"golf","created_at":"2026-04-10T02:03:09.614829621Z","created_by":"coding","updated_at":"2026-04-10T08:26:46.161560106Z","closed_at":"2026-04-10T08:26:46.161384067Z","close_reason":"Implemented Component 36: Guided Troubleshooting with proactive contextual help.\n\nBackend:\n- FleetNotifier: Track node offline events with 2-hour threshold before triggering help\n- EditTracker: Monitor repeated settings changes (3+ within 60min triggers hint)\n- ZoneQualityTracker: Detect degraded detection quality (>24h below 60%)\n- DiscoveryTracker: First-time feature discovery tooltips (shown once per feature)\n- Manager: Coordinates all guided troubleshooting features with 5min periodic checks\n- REST API: /api/guided/* endpoints for issues, tooltips, feedback, calibration\n\nFrontend:\n- tooltip.js: Feature discovery tooltips with server-side coordination\n- tooltips.js: Sequential tooltip tour manager for first-time users \n- troubleshoot.js: Troubleshooting manager for quality/offline/calibration events\n- guided-help.js: Step-by-step guidance content\n\nTriggers:\n- Detection quality drops below 60% for >24 hours\n- Same setting key modified 3+ times in 60 minutes\n- Node offline for >2 hours\n- First-time feature access (tooltips)\n- After false positive feedback (explanation + adjustment offer)\n- After successful calibration (positive reinforcement)\n\nDesign principles:\n- Reactive, not proactive: Help appears only when something seems wrong\n- Dismissible in one tap: Never blocks the UI\n- Never repeats after dismissal (stored in localStorage + server)\n- Always explains what will happen next\n- Never condescending: Assumes the user is intelligent","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","failure-count:10","mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} {"id":"spaxel-esn","title":"Create synthetic walkers","description":"Implement synthetic walkers that move through the virtual space between nodes.\n\nAcceptance:\n- Walkers can traverse between virtual nodes\n- Movement patterns produce realistic synthetic data","status":"closed","priority":2,"issue_type":"task","assignee":"hotel","created_at":"2026-04-09T16:11:25.513037845Z","created_by":"coding","updated_at":"2026-04-09T17:15:23.000870233Z","closed_at":"2026-04-09T17:15:23.000764431Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["failure-count:1","mitosis-child","mitosis-depth:1","parent-spaxel-d41"]} {"id":"spaxel-ez4","title":"Detection explainability overlay","description":"## Background\n\nWhen a blob appears in an unexpected position, or an alert fires that seems wrong, the first question is \"why?\" The explainability overlay answers this question visually in the 3D scene, without requiring the user to understand deltaRMS, Fresnel zones, or UKF — though the data is available for those who want it. This transforms a \"magic box\" into a comprehensible physical system.\n\nThis is also the most important debugging tool for a developer tuning the system: seeing which links contributed most to a blob position, and by how much, is the fastest path to understanding localisation errors.\n\n## ExplainabilitySnapshot\n\nThe FusionEngine (spaxel-m9a) is extended to emit an ExplainabilitySnapshot alongside each BlobUpdate. This snapshot contains all the data needed to explain why a specific blob appeared at a specific position.\n\nExplainabilitySnapshot struct (mothership/internal/fusion/explain.go):\n- blob_id: the ID of the blob being explained\n- blob_position: Vec3 — final estimated position\n- per_link_contributions: []LinkContribution\n - link_id, tx_mac, rx_mac\n - weight float64 — the geometric Fresnel weight for this blob position\n - learned_weight float64 — the learned spatial weight (from weight learner, Phase 7)\n - combined_weight float64 = weight * learned_weight\n - delta_rms float64 — the current deltaRMS for this link\n - contribution_pct float64 — percentage of total fusion score contributed by this link\n - fresnel_intersection_volume float64 — volume of Fresnel zone ellipsoid that overlaps the blob's voxel (proxy for \"how much does this link see this position\")\n- ble_match: optional — if identity is matched: {device_mac, person_id, person_label, ble_distance_m, triangulation_confidence}\n- fusion_score float64 — total occupancy grid score at blob position\n- timestamp of snapshot\n\nThe snapshot is broadcast via WebSocket as \"blob_explain\" message type, alongside the regular \"blob_update\". The frontend requests a snapshot by sending {\"type\":\"request_explain\",\"blob_id\":\"...\"} — the server then enriches the next blob update with the explain data.\n\n## 3D Explain Mode UI\n\nRight-click (desktop) or long-press (mobile, 300ms) on any blob/track in the Three.js scene triggers explain mode.\n\nScene transformation in explain mode:\n1. All link lines dim to 20% opacity (using THREE.MeshBasicMaterial.opacity)\n2. Contributing links — those with contribution_pct > 2% — increase to 100% opacity and glow with colour intensity mapped to contribution_pct (low contribution = pale blue, high contribution = bright yellow)\n3. First Fresnel zone ellipsoids rendered for each contributing link: THREE.Mesh with SphereGeometry scaled by (a, b, b) and rotated to the link axis, translucent wireframe + fill (opacity 0.1). The ellipsoid colour matches the link line colour.\n4. A \"blob explanation panel\" (sidebar overlay, not a Three.js object) shows the breakdown:\n - Blob position in metres: \"Detected at (3.2m, 1.8m, 1.0m)\"\n - Fusion score: \"Detection confidence: [N]%\"\n - Contributing links table: link name, contribution %, deltaRMS, health score — sorted by contribution descending\n - Motion sparkline: small 30-second deltaRMS chart per link (uses the recording buffer data if available, otherwise the in-memory history)\n - BLE match details: \"Identity: Alice (BLE triangulation, confidence 82%, 0.4m from blob)\"\n - If no BLE match: \"Identity: Unknown (no BLE device match)\"\n\nExit explain mode: click anywhere outside the blob, or press Escape. Scene returns to normal opacity levels.\n\n## Fresnel Ellipsoid Geometry\n\nThe first Fresnel zone ellipsoid geometry for a link:\n- TX position P1, RX position P2\n- Link distance d = |P1 - P2|\n- WiFi wavelength lambda = 0.06m (5 GHz) or 0.125m (2.4 GHz) — use the channel from the node's hello message\n- Semi-major axis: a = (d + lambda/2) / 2\n- Semi-minor axis: b = sqrt(a^2 - (d/2)^2)\n- Centre: midpoint(P1, P2)\n- Orientation: the major axis is along the P1->P2 unit vector\n\nIn Three.js: SphereGeometry with radius=1, then scale (a, b, b) with the correct rotation matrix (use THREE.Quaternion.setFromUnitVectors to align with P1->P2 direction).\n\n## Motion Sparkline\n\nFor each contributing link in the explanation panel, show a 30-second history of deltaRMS as a small canvas sparkline (using the existing amplitude history if available from the dashboard WebSocket connection, or fetching from GET /api/recordings/{link_id}/recent?seconds=30 if the recording buffer is available).\n\nThe sparkline shows the moment of detection as a vertical line at the right edge. A horizontal dashed line shows the current motion threshold. Visually conveying \"the signal crossed the threshold at this moment.\"\n\n## Files to Create or Modify\n\n- mothership/internal/fusion/explain.go: ExplainabilitySnapshot, emission logic in FusionEngine\n- mothership/internal/fusion/engine.go: extend to emit ExplainabilitySnapshot alongside BlobUpdate\n- dashboard/js/explain.js: explain mode 3D scene transforms, sidebar panel\n- dashboard/js/fresnel.js: Fresnel ellipsoid geometry helper (reused by Fresnel debug overlay bead)\n- mothership/internal/dashboard/hub.go: blob_explain WebSocket message type\n\n## Tests\n\n- Test ExplainabilitySnapshot generation: with 3 known links and a blob at a known position, verify per_link_contributions are computed correctly\n- Test contribution_pct sums to approximately 100% across all links with non-zero weight\n- Test Fresnel ellipsoid geometry: for TX at (0,0,0) and RX at (4,0,0) with lambda=0.06: a ≈ 2.015, b ≈ 0.345. Verify these values from the geometry computation.\n- Test that explain mode correctly dims/highlights links in the Three.js scene (test via scene state inspection, not visual rendering)\n- Test that WebSocket \"request_explain\" message triggers snapshot emission in the next update cycle\n- Test sidebar panel rendering with mock ExplainabilitySnapshot data\n\n## Acceptance Criteria\n\n- Right-click on any blob triggers explain mode with correct contributing link highlighting\n- Fresnel ellipsoids render at correct positions and sizes for all contributing links\n- Confidence breakdown panel shows per-link contributions that sum to 100%\n- Non-contributing links visually dimmed in explain mode\n- Motion sparklines show 30-second history for each contributing link\n- BLE match details shown when identity is available\n- Escaping explain mode restores all link opacities to normal\n- Tests pass","status":"open","priority":3,"issue_type":"task","created_at":"2026-03-28T01:55:18.006377304Z","created_by":"coding","updated_at":"2026-03-28T03:29:14.817464555Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"spaxel-ez4","depends_on_id":"spaxel-i28","type":"blocks","created_at":"2026-03-28T03:29:14.817442776Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-ez4","depends_on_id":"spaxel-s70","type":"blocks","created_at":"2026-03-28T01:55:20.955603637Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-fi6","title":"Implement Portals CRUD REST endpoints","description":"Implement CRUD endpoints for portals: GET/POST /api/portals, PUT/DELETE /api/portals/{id}. Include OpenAPI-style godoc comments. Portal changes must reflect in live 3D view within one WebSocket cycle.","status":"closed","priority":2,"issue_type":"task","assignee":"foxtrot","created_at":"2026-04-07T13:56:27.334232115Z","created_by":"coding","updated_at":"2026-04-07T17:56:13.860592476Z","closed_at":"2026-04-07T17:56:13.860493596Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred","failure-count:2","mitosis-child","mitosis-depth:1","parent-spaxel-21n"]} @@ -148,7 +148,7 @@ {"id":"spaxel-tim","title":"Adaptive sensing rate","description":"Implement mothership-controlled sensing rate with on-device burst detection.\n\n## Deliverables\n- Mothership rate control: send rate change commands (2Hz idle ↔ 50Hz active) per link via WebSocket\n- Rate decision logic: drop to 2Hz when no motion for 30s, burst to 50Hz on motion\n- On-device amplitude variance check at low rate (2Hz) — if variance exceeds threshold, burst to full rate and notify mothership\n- Motion hints from ESP32 to preemptively ramp adjacent links\n- Firmware changes: firmware/main/csi.c to support dynamic rate changes\n\n## Acceptance Criteria\n- Idle links automatically drop to 2Hz packet rate\n- Motion triggers burst to configured active rate\n- ESP32 can locally detect motion onset and self-burst before mothership commands\n- Adjacent links ramp up on motion hints from nearby nodes\n- Tests for rate decision logic in mothership\n\n## References\n- Plan: docs/plan/plan.md item 12\n- CSI capture: firmware/main/csi.c\n- Signal pipeline: mothership/internal/signal/processor.go","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","created_at":"2026-03-27T01:56:21.876231481Z","created_by":"coding","updated_at":"2026-03-28T05:36:02.592406514Z","closed_at":"2026-03-28T05:36:02.592346365Z","close_reason":"Implemented: ratecontrol.go (bcfd1e3, fb69190) — mothership-controlled 2Hz↔50Hz rate changes, firmware csi.c/websocket.c dynamic rate config, on-device amplitude variance burst detection","source_repo":".","compaction_level":0,"original_size":0} {"id":"spaxel-tqj","title":"CSI recording buffer","description":"Implement disk-backed circular buffer for CSI frame recording.\n\n## Deliverables\n- New package: mothership/internal/recording/\n- Append incoming CSI frames to disk-backed circular buffer (48h default retention)\n- Binary format for efficient storage (same frame format as WebSocket)\n- Configurable retention period via environment variable\n- Foundation for time-travel replay feature (Phase 8)\n\n## Acceptance Criteria\n- CSI frames are persisted to disk as they arrive\n- Buffer auto-prunes frames older than retention period\n- Can read back frames for a given time range\n- Storage is bounded (auto-prune prevents disk exhaustion)\n- Tests cover write, read-back, and pruning\n\n## References\n- Frame format: mothership/internal/ingestion/frame.go (24-byte header + payload)\n- Ring buffer: mothership/internal/ingestion/ring.go (in-memory reference)","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","created_at":"2026-03-27T01:56:09.947974130Z","created_by":"coding","updated_at":"2026-03-28T01:34:05.658150061Z","closed_at":"2026-03-27T03:02:15.596740568Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"spaxel-tqj","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T01:34:05.658124716Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-tr7","title":"3D spatial visualization","description":"Full Three.js 3D scene with humanoid figures, room bounds, and node meshes.\n\n## Deliverables\n- Room bounds visualization (walls, floor, ceiling from configured dimensions)\n- Floor plan texture support (uploaded image mapped to ground plane)\n- Humanoid figures using SkinnedMesh + AnimationMixer (standing/walking/seated/lying)\n- Vertical pillar anchors and footprint trails for tracked blobs\n- Node meshes at configured 3D positions with link lines between pairs\n- View presets (top-down, perspective, first-person follow)\n- WebSocket integration to receive blob positions from mothership\n\n## Acceptance Criteria\n- Humanoid figures animate smoothly between postures\n- User can orbit, pan, zoom with OrbitControls\n- Node positions and link lines update in real-time\n- Works with existing dashboard skeleton (dashboard/js/app.js)\n\n## References\n- Plan: docs/plan/plan.md item 17\n- Dashboard: dashboard/index.html, dashboard/js/app.js","status":"closed","priority":2,"issue_type":"task","assignee":"spaxel-alpha","created_at":"2026-03-27T01:57:04.504533558Z","created_by":"coding","updated_at":"2026-03-28T05:36:26.148595152Z","closed_at":"2026-03-28T05:36:26.148493523Z","close_reason":"Implemented: dashboard/js/viz3d.js 566 lines (bcd19ad) — room bounds, humanoid SkinnedMesh 13-bone skeleton 4 postures, footprint trails, node meshes, link lines, 3 view presets","source_repo":".","compaction_level":0,"original_size":0,"labels":["deferred"],"dependencies":[{"issue_id":"spaxel-tr7","depends_on_id":"spaxel-cxm","type":"blocks","created_at":"2026-03-28T03:29:13.740185994Z","created_by":"coding","metadata":"{}","thread_id":""}]} -{"id":"spaxel-trsm","title":"Make expert mode mobile-responsive","description":"Touch orbit/pan/zoom functionality for mobile devices.","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-10T02:03:09.684731821Z","created_by":"coding","updated_at":"2026-04-10T02:03:09.684731821Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} +{"id":"spaxel-trsm","title":"Make expert mode mobile-responsive","description":"Touch orbit/pan/zoom functionality for mobile devices.","status":"in_progress","priority":2,"issue_type":"task","assignee":"golf","created_at":"2026-04-10T02:03:09.684731821Z","created_by":"coding","updated_at":"2026-04-10T08:27:17.474878224Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["mitosis-child","mitosis-depth:1","parent-spaxel-17u"]} {"id":"spaxel-ts2","title":"Implement image upload endpoint","description":"## Task\nImplement POST /api/floorplan/image endpoint.\n\n## Specification\n- Multipart form handling\n- Accept PNG/JPG max 10 MB\n- Save to /data/floorplan/image.png\n- Reject > 10 MB upload with 413 error\n\n## Acceptance\n- Image upload saves file to /data/floorplan/image.png\n- > 10 MB upload rejected with 413 error","status":"closed","priority":2,"issue_type":"task","assignee":"charlie","created_at":"2026-04-07T17:55:50.419717078Z","created_by":"coding","updated_at":"2026-04-07T18:43:14.895237433Z","closed_at":"2026-04-07T18:43:14.895178829Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["failure-count:2","mitosis-child","mitosis-depth:1","parent-spaxel-klk"]} {"id":"spaxel-tvq","title":"Command palette","description":"## Background\n\nExpert users — system installers, power users, home automation enthusiasts — want to operate the dashboard entirely from the keyboard. Reaching for the mouse to navigate between nodes, zones, and features breaks focus. The command palette (Ctrl+K / Cmd+K) provides a keyboard-driven interface to every feature: navigate to any person, zone, or node by typing their name, execute commands like \"Start OTA\" or \"Toggle Fresnel overlay\", and even jump the 3D scene to specific timestamps using a time syntax.\n\n## Trigger and Appearance\n\nKeyboard shortcut: Ctrl+K (Windows/Linux) or Cmd+K (macOS). Also triggerable via a small search icon button in the dashboard header bar.\n\nAvailable in: expert mode only. Not available in simple mode or ambient mode (those are for non-technical users who don't benefit from a command palette).\n\nThe palette appears as a centred modal overlay:\n- Semi-transparent dark backdrop (opacity 0.5, backdrop-filter: blur(4px))\n- Centred container: max-width 600px, border-radius 12px, background #1e293b\n- Top: search input (auto-focused, placeholder \"Search people, zones, nodes, commands...\")\n- Below: results list (max 8 visible, scrollable)\n\nClose on: Escape key, click on backdrop, second Ctrl+K/Cmd+K.\n\n## Search Scope\n\nAll entities are cached client-side in the dashboard's state store (already maintained from WebSocket updates). No server round-trips for search — purely client-side matching.\n\nSearch categories (in priority order in results):\n1. People: names from the people registry. Icon: person silhouette. Action: jump 3D camera to person's current blob.\n2. Zones: zone names. Icon: location pin. Action: jump 3D camera to zone centroid, select zone.\n3. Nodes: node labels or MAC addresses. Icon: radio antenna. Action: jump 3D camera to node position, select node.\n4. Recent events: last 20 timeline events (titles). Icon: clock. Action: open event detail in timeline.\n5. Commands: static list of dashboard commands (see Commands below). Icon: lightning bolt. Action: execute command.\n6. Time navigation: if query starts with \"@\", attempt time parsing (see below). Icon: clock. Action: seek replay to that time.\n\n## Fuzzy Matching\n\nClient-side fuzzy matching for all categories. Use a simple Jaro-Winkler or Levenshtein-distance based scorer:\n- Exact prefix match: highest score. \"Kit\" -> \"Kitchen\" gets top score.\n- Subsequence match: \"kitch rm\" -> \"Kitchen\" matches (not adjacent characters but in order).\n- Typo tolerance: 1 character substitution tolerated for strings > 4 characters.\n\nImplementation: a custom 30-line fuzzy match function in JavaScript. No dependencies needed. The function returns a score in [0, 1]; results with score < 0.3 are excluded.\n\nSort: primary by category priority (commands highest if starting with \"/\"), secondary by match score.\n\n## Commands\n\nStatic list of commands accessible from the palette:\n\nNavigation commands:\n- \"Open settings\" -> navigate to /settings\n- \"Open fleet page\" -> navigate to /fleet\n- \"Open automations\" -> navigate to /automations\n- \"Open simulator\" -> navigate to /simulate\n\nView commands:\n- \"Toggle Fresnel overlay\" -> toggle the Fresnel zone debug layer\n- \"Toggle flow map\" -> toggle the flow map layer\n- \"Toggle dwell heatmap\" -> toggle the dwell heatmap layer\n- \"Toggle zone volumes\" -> toggle zone cuboid visibility\n- \"Reset camera\" -> fly camera back to default top-down position\n\nSystem commands:\n- \"Enter away mode\" -> POST /api/mode {\"mode\":\"away\"}\n- \"Enter home mode\" -> POST /api/mode {\"mode\":\"home\"}\n- \"Enter sleep mode\" -> POST /api/mode {\"mode\":\"sleep\"}\n- \"Trigger fleet OTA\" -> opens the fleet OTA dialog\n- \"Add a person\" -> opens the Add Person form in the People & Devices panel\n- \"Add a zone\" -> starts zone creation mode in the 3D view\n- \"Add a portal\" -> starts portal creation mode\n\nDebug commands (shown at bottom, lower priority):\n- \"Export all events CSV\" -> GET /api/events?format=csv and download\n- \"Show link health table\" -> opens the link health panel\n- \"Run diagnostics\" -> triggers a diagnostics pass and shows results\n- \"Check firmware updates\" -> fetches latest firmware version and compares to all nodes\n\n## Time Navigation\n\nIf the query starts with \"@\": attempt to parse as a time expression and offer a \"Jump to time\" result.\n\nSupported formats:\n- \"@3am\" -> today at 03:00\n- \"@3:15am\" -> today at 03:15\n- \"@yesterday 11pm\" -> yesterday at 23:00\n- \"@2026-03-27 14:23\" -> specific datetime\n- \"@-30min\" -> 30 minutes ago from now\n- \"@-2h\" -> 2 hours ago\n\nOn selection: triggers time-travel replay to the parsed timestamp (same as clicking a timeline event's tap-to-jump).\n\nShow parsing preview in the result item: \"Jump to 2026-03-27 03:00:00\" with a clock icon.\n\n## Result Item Rendering\n\nEach result item in the list:\n- Left: icon (category-appropriate SVG, 16px)\n- Centre-left: primary text (entity name or command label, 14px)\n- Centre-right: secondary text (grey, 12px): for zones: \"[N] people currently\", for nodes: \"[online/offline]\", for commands: keyboard shortcut hint if any\n- Right: arrow icon (shows the item is actionable)\n\nSelected item (keyboard navigation): blue #3b82f6 background highlight.\n\n## Keyboard Navigation\n\n- Arrow up/down: move selection through results\n- Enter: execute selected item\n- Tab: same as Enter (for keyboard-first users who use Tab to confirm)\n- Escape: close palette\n- Character keys: type to refine search (selection resets to first result on each keystroke)\n\n## Recent History\n\nWhen the palette opens with an empty query: show \"Recent\" header with the last 5 palette actions (stored in localStorage \"spaxel_palette_history\"). Format: same as search results but without scores. \"Recent\" category shown before search results.\n\nRecent history excludes time navigation entries (those are ephemeral).\n\n## Files to Create or Modify\n\n- dashboard/js/commandpalette.js: CommandPaletteManager, fuzzy match, time parsing, result rendering\n- dashboard/js/commandpalette.css: modal overlay, input, result list styles\n- dashboard/js/app.js: keyboard shortcut listener (Ctrl+K / Cmd+K), integrate CommandPaletteManager\n\n## Tests\n\n- Test fuzzy matching: \"kit\" -> \"Kitchen\" score > 0.7; \"livig rm\" -> \"Living Room\" score > 0.5; \"xyz\" -> \"Kitchen\" score < 0.3 (excluded)\n- Test time navigation parsing: \"@3am\" parses to today at 03:00; \"@-30min\" parses to 30 minutes ago; \"@2026-03-27 14:23\" parses correctly\n- Test that commands list is complete (all documented commands present in the registry)\n- Test keyboard navigation: arrow down moves selection, Enter executes, Escape closes\n- Test recent history: execute 5 actions, open palette with empty query -> 5 recent items shown\n- Test that palette does not activate in simple mode or ambient mode (keyboard listener absent)\n- Test that viewport reposition correctly positions the palette centred on the screen\n\n## Acceptance Criteria\n\n- Command palette opens with Ctrl+K (or Cmd+K on macOS) in < 50ms\n- Fuzzy search returns \"Kitchen\" for query \"kitch\", \"kit\", \"ktchn\"\n- Time navigation \"@3am\" correctly seeks replay to 03:00 today\n- All documented commands are accessible and execute correctly\n- Arrow key navigation works correctly through results\n- Recent history shows last 5 palette actions on empty query\n- Palette unavailable in simple and ambient modes\n- All features accessible in 3 keystrokes or fewer from palette open\n- Tests pass","status":"open","priority":3,"issue_type":"task","created_at":"2026-03-28T02:02:28.058307267Z","created_by":"coding","updated_at":"2026-03-28T03:29:15.028133142Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"spaxel-tvq","depends_on_id":"spaxel-s70","type":"blocks","created_at":"2026-03-28T02:02:31.595593278Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-tvq","depends_on_id":"spaxel-sl2","type":"blocks","created_at":"2026-03-28T03:29:15.028093011Z","created_by":"coding","metadata":"{}","thread_id":""}]} {"id":"spaxel-u7y","title":"Firmware: NTP clock sync for TX stagger accuracy","description":"## Overview\nImplement NTP synchronization on ESP32-S3 so all nodes share a common clock, enabling accurate TX stagger scheduling to avoid CSI collisions.\n\n## Firmware (firmware/main/wifi.c or ntp.c)\n- Call esp_sntp_setservername(0, ntp_server) before esp_sntp_init() on boot\n- ntp_server read from NVS 'ntp_server' key (default: 'pool.ntp.org')\n- Attempt sync for up to 10 seconds after WiFi connect; log WARN to UART if sync fails\n- On sync failure: proceed without stagger (rely on CSMA/CA for collision avoidance)\n- Resync every 10 minutes via esp_timer periodic callback\n- Include NTP sync status in health JSON message: {type:'health', ..., ntp_synced: true/false}\n\n## Mothership (provisioning payload)\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\n## Acceptance\n- Node health messages show ntp_synced: true when pool is reachable\n- ntp_synced: false when NTP blocked — node still operates normally\n- Resync occurs every ~600s (verified via UART logs)","status":"closed","priority":2,"issue_type":"task","assignee":"alpha","created_at":"2026-04-06T16:42:26.894640218Z","created_by":"coding","updated_at":"2026-04-07T17:46:19.521425117Z","closed_at":"2026-04-07T17:46:19.521245004Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["failure-count:1"],"dependencies":[{"issue_id":"spaxel-u7y","depends_on_id":"spaxel-288","type":"blocks","created_at":"2026-04-07T14:37:00.359263571Z","created_by":"coding","metadata":"{}","thread_id":""},{"issue_id":"spaxel-u7y","depends_on_id":"spaxel-qgj","type":"blocks","created_at":"2026-04-07T14:37:00.321904383Z","created_by":"coding","metadata":"{}","thread_id":""}]} diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index 23abf24..f30ed56 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -6b22ba65acb3e583cdd7cb786d7104402f85a495 +b583990d434dd472d8bac8cfb60f3129ff93e56a diff --git a/dashboard/index.html b/dashboard/index.html index 2de30f8..a298ba8 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -38,6 +38,7 @@ #scene-container { width: 100vw; height: 100vh; + touch-action: none; /* Prevent default touch behaviors for OrbitControls */ } /* Status bar at top */ @@ -2181,6 +2182,297 @@ background: rgba(255, 255, 255, 0.15); } + /* ===== Mobile Menu Toggle Button ===== */ + #mobile-menu-btn { + display: none; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #ccc; + font-size: 14px; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s, color 0.2s; + } + + #mobile-menu-btn:hover { + background: rgba(255, 255, 255, 0.15); + color: #fff; + } + + #mobile-menu-btn svg { + width: 18px; + height: 18px; + vertical-align: middle; + } + + /* ===== Mobile Responsive Styles ===== */ + @media (max-width: 768px) { + /* Show mobile menu button */ + #mobile-menu-btn { + display: block; + order: -1; /* Move to beginning of status bar */ + } + + /* Status bar - compact on mobile */ + #status-bar { + height: auto; + flex-wrap: wrap; + padding: 8px; + gap: 8px; + font-size: 12px; + } + + .status-item { + font-size: 11px; + } + + /* Hide less important status items on mobile */ + .status-item:has(#fps-counter), + .status-item:has(#link-count) { + display: none; + } + + /* Node panel - collapsible on mobile */ + #node-panel { + top: auto; + bottom: 0; + left: 0; + right: 0; + width: auto; + max-height: 50vh; + border-radius: 12px 12px 0 0; + transform: translateY(calc(100% - 40px)); + transition: transform 0.3s ease; + } + + #node-panel.expanded { + transform: translateY(0); + } + + /* Chart panel - smaller on mobile */ + #chart-panel { + width: calc(100% - 16px); + right: 8px; + left: 8px; + bottom: 8px; + height: 200px; + } + + /* Presence panel - hide on mobile (use timeline instead) */ + #presence-panel { + display: none; + } + + /* Buttons - smaller on mobile */ + .view-btn, #gdop-toggle-btn, #fresnel-toggle-btn, #room-editor-btn, + #floorplan-btn, #simulator-btn, #pause-live-btn, #ble-btn, + #settings-btn, #add-node-btn, #add-virtual-btn { + padding: 4px 8px; + font-size: 11px; + } + + /* Detection quality gauge - compact */ + #detection-quality { + padding: 2px 6px; + } + + #quality-gauge-container { + width: 24px; + height: 24px; + } + + #quality-value { + font-size: 7px; + } + + #quality-label { + display: none; + } + + /* Room editor panel - full width on mobile */ + #room-editor-panel { + top: auto; + bottom: 0; + left: 0; + right: 0; + width: auto; + border-radius: 12px 12px 0 0; + transform: translateY(calc(100% - 40px)); + transition: transform 0.3s ease; + } + + #room-editor-panel.expanded { + transform: translateY(0); + } + + /* GDOP legend - smaller on mobile */ + #gdop-legend { + left: 8px; + bottom: 220px; + padding: 8px; + } + + #gdop-gradient { + width: 100px; + } + + /* Toast notifications - full width on mobile */ + #toast-container { + bottom: 220px; + left: 8px; + right: 8px; + transform: none; + } + + .toast { + font-size: 13px; + padding: 10px 16px; + } + + /* Wizard - full screen on mobile */ + #wizard-card { + width: 100%; + height: 100vh; + max-height: 100vh; + border-radius: 0; + padding: 20px; + } + + /* Onboarding illustration - smaller on mobile */ + .esp32-illustration { + max-width: 200px; + } + + /* Anomaly banner - smaller on mobile */ + .anomaly-banner { + padding: 16px; + margin: 0 8px; + } + + /* Replay control bar - compact on mobile */ + .replay-control-bar { + width: calc(100% - 16px); + padding: 10px 16px; + flex-wrap: wrap; + justify-content: center; + } + + .replay-info { + order: -1; + width: 100%; + text-align: center; + margin-bottom: 8px; + } + + /* Timeline - full screen on mobile */ + #timeline-view { + padding: 0; + } + + .timeline-header { + padding: 12px; + } + + .timeline-filter-bar { + padding: 12px; + } + + /* Simulator panel - full screen on mobile */ + .simulator-panel { + width: 100%; + height: 100vh; + border-radius: 0; + } + + /* Briefing card - full width on mobile */ + #briefing-card { + width: calc(100% - 16px); + margin: 8px; + } + + /* Quick actions - larger touch targets on mobile */ + .quick-action-item { + min-height: 48px; + padding: 12px 16px; + } + + /* Command palette - full width on mobile */ + .command-palette-overlay { + padding: 8px; + } + + .command-palette { + max-width: 100%; + } + + /* Morning briefing - adjust spacing */ + #morning-briefing-panel { + width: calc(100% - 16px); + margin: 8px; + } + } + + /* Touch device optimizations */ + @media (hover: none) and (pointer: coarse) { + /* Larger touch targets for touch devices */ + .view-btn, #gdop-toggle-btn, #fresnel-toggle-btn, #room-editor-btn, + #floorplan-btn, #simulator-btn, #pause-live-btn, #ble-btn, + #settings-btn, #add-node-btn, #add-virtual-btn { + min-height: 44px; + min-width: 44px; + padding: 8px 12px; + } + + /* Node items - larger touch targets */ + .node-item { + padding: 12px 8px; + min-height: 44px; + } + + /* Link items - larger touch targets */ + .link-item { + padding: 10px 8px; + min-height: 44px; + } + + /* Toast notifications - larger on touch devices */ + .toast { + padding: 14px 20px; + font-size: 14px; + } + + /* Replay controls - larger touch targets */ + .replay-btn { + width: 44px; + height: 44px; + } + + /* Timeline events - larger touch targets */ + .timeline-event-item { + padding: 16px; + } + + /* Disable hover effects on touch devices */ + .node-item:hover, + .link-item:hover, + .view-btn:hover, + #gdop-toggle-btn:hover, + #fresnel-toggle-btn:hover, + .room-field input:hover { + background: initial; + } + + /* Use active states instead */ + .node-item:active, + .link-item:active, + .view-btn:active, + #gdop-toggle-btn:active, + #fresnel-toggle-btn:active { + background: rgba(255, 255, 255, 0.15); + } + } + /* ===== Replay Control Bar ===== */ #pause-live-btn { background: rgba(255, 193, 7, 0.15); @@ -2526,6 +2818,13 @@ Disconnected +
Nodes: 0
@@ -3040,6 +3339,79 @@