The EventBus pub-sub mechanism in mothership/internal/events/bus.go was
already implemented with all required EventType constants, typed payload
structs, fan-out subscriber support, and comprehensive tests. This commit
also wires ConsumeExplainRequests into the fusion loop for dashboard clients.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three issues resolved in the detection explainability system:
- computeExplanation returned early when grid==nil (always the case in the
live fusion loop), causing all explain requests to return empty link
contributions. Removed the unnecessary nil-grid guard since the Fresnel
computation uses only blob/link positions, not the grid.
- Contribution values were raw deltaRMS×weight×zoneDecay scalars, not
percentages. Now normalized so contributing links sum to 1.0, giving the
dashboard a proper confidence breakdown (60% / 40% / etc.).
- Fixed off-by-one in blobHistory eviction (kept 101 instead of 100).
Added 23 table-driven tests covering: nil-grid computation, single/multi-link
normalization, Fresnel zone number geometry, ellipsoid generation, HTTP
handlers (200/400 paths), WebSocket snapshot fields, BLE match inclusion,
zone decay inverse-square law, and history eviction cap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add security status card with arm/disarm dialog, DISARMED/LEARNING/ARMED/ALERT
badge, learning progress bar (N of 7 days), and last-anomaly summary line
- Add full-width alert banner with acknowledge button for armed-mode anomalies;
acknowledged alerts disappear from banner but remain in history
- Add anomaly timeline panel (24h) with severity scores and timeline navigation
- Fix WS broadcast field names to match AnomalyEvent JSON/REST API:
anomaly_type→type, timestamp_ms→RFC3339 timestamp so JS handles both
WS pushes and polled history uniformly
- Fix formatTimeAgo() to parse RFC3339 string timestamps in addition to Unix-ms
- Fix fetchAnomalyCount() to use /api/anomalies?since=24h (structured response)
instead of /api/anomalies/history (returns plain array)
- Add security-card detail area styling to anomaly.css
- Add BlobIdentityProvider wiring in zones API for people resolution in zone responses
- Add linkweather diagnostic engine tests (Rules 1-5 + helpers)
All go test ./... pass; go vet ./... clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fresnel zone ellipsoids render for all active links when the debug layer
is toggled on. Uses shared fresnel.js helper for geometry computation,
with hover tooltips showing link details and click-to-select. viz3d.js
refactored to use the shared module instead of duplicating calculations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The showFresnelTooltip function referenced data.txMAC/data.rxMAC which
don't exist on the ellipsoid geometry data object. Use link.nodeMAC and
link.peerMAC from the link state instead, which are always available.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simplify Fresnel debug overlay toggle to use single toggleFresnelDebugOverlay
call instead of duplicating Viz3D sync logic. Fix semi-minor axis calculation
in viz3d.js (was missing factor of 2 in distance term). Add Math.max(0,...)
guard against negative sqrt. Add missing dispose mocks in fresnel tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire Fresnel zone toggle through Layers module for consistent
state management across toolbar, debug panel, and layer controls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Expose the OUI-looked-up manufacturer field in the /api/fleet endpoint
so the fleet page can display hardware vendor info alongside each node.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Improve link highlighting with Fresnel health color mapping
- Route re-provision through live page via query param for reliability
- Remove simple-mode gating from command palette (now available everywhere except ambient)
- Update Fresnel tests, fleet page unpaired node UX, and help articles
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add 24h migration window for legacy/unprovisioned nodes to connect while
flagged as Unpaired rather than silently rejected. Surface unpaired nodes
in the fleet UI with amber badge, pulsing status indicator, and migration
window countdown banner. Add re-provision wizard entry point from fleet
panel — clicking "Pair" on an unpaired node opens the onboarding wizard
in re-provisioning mode, skipping firmware flash and going straight to
serial credential provisioning. After migration window closes, nodes
without valid tokens are rejected outright.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three fleet handler tests (TestFleetWithVirtualNodes, TestFleetWithNoNodes,
TestFleetWithUnpairedNode) were decoding the API response as []FleetNode
instead of the wrapped fleetListResponse struct. Fixed to use fleetListResp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Timeline event clicks now always trigger jump-to-time replay regardless
of dashboard mode, not just in expert mode. Simplified handleSeek to
remove the dashboardMode state variable and expert mode check.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix default mothership URL from /ws to /ws/node to match the ingestion
server's actual WebSocket endpoint. Remove unused updateWalkerPosition
and walkerDistanceToNode functions from walker.go (the walker update
logic lives in main.go's updateWalkers).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rename expert.css → scene.css (file contained WCAG touch targets and
3D scene layout, nothing mode-specific). Remove dead dashboardMode !==
'expert' guard in timeline seek handler. Simple/expert toggle and
localStorage key were already removed in prior commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix byte constant overflow for noise_floor field by using an int8
variable instead of a constant conversion expression. Also update
channel field to use --channel flag value instead of hardcoded 6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Simple/Expert toggle was removed in an earlier commit but left behind
stale comments referencing "mode toggle bar", "expert mode", and
"simple.html" in CSS files. Clean these up to avoid confusion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire sidebar timeline handleSeek to navigate to timeline view in simple
mode instead of attempting replay. Fix onRouterModeChange to properly
detect expert vs simple modes based on route name. All 41 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the full explainability overlay for understanding why a blob was detected:
- ExplainabilitySnapshot generation with per-link contribution tracking and zone decay
- Fresnel zone ellipsoid geometry computation and 3D wireframe rendering
- WebSocket request_explain / blob_explain flow for on-demand snapshots
- Right-click, long-press, click, and hover tooltip activation paths
- X-ray overlay dims non-contributing elements, highlights contributing links
- Sidebar panel with confidence gauge, links table, sparklines, BLE match card
- Escape key and backdrop click to exit, restoring scene state
Also includes: simple mode removal, CSS cleanup, fleet page enhancements, sidebar timeline fixes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix missing colons in CSS declarations across timeline.css and replay.css
that broke gap/padding/margin/bottom properties. Simplify timeline mode
detection to use router callbacks instead of SpaxelSimpleModeDetection.
Add aria-labels to select elements for accessibility.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Nodes that connect without a valid token during the migration window are
now accepted and flagged as Unpaired rather than rejected. Fleet health
surfaces the flag so the dashboard can show an amber Unpaired badge in
the MAC column, an "Unpaired" status badge, and a ↺ re-provision action
button that re-opens the onboarding wizard in reprove mode (skips the
firmware-flash step, targets the specific node's MAC during detect).
- ingestion/server: migrationDeadline + Unpaired flag on NodeConnection
- fleet/fleethandler: UnpairedProvider interface, merges unpaired MACs
into fleet health response
- config: SPAXEL_MIGRATION_WINDOW_HOURS (default 24 h, range 0-168)
- main: wires migration deadline and unpaired provider at startup
- onboard.js: reprove(mac) public API, skip-flash + targeted-detect mode
- fleet.js: Unpaired badge, Re-provision button, unpaired banner, ⚠ in
role list, reproveNode public API
- wizard.css: .wizard-reprove-banner amber styling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sidebar timeline module was not listening for SpaxelSimpleModeDetection
mode changes, causing the tap-to-jump simple mode test to fail. Added
onSimpleModeChange handler and registration so the sidebar correctly
switches to simple mode and navigates to timeline view instead of replay.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add @axe-core/playwright with Playwright test runner that asserts zero
WCAG 2A/2AA violations across index, live, fleet, setup, and
integrations pages. Fix contrast violations on integrations page: use
darker blue for primary buttons, lighter text for descriptions/hints,
and add body-level dark background in layout.css.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Timeline events now delegate to SpaxelReplay.jumpToTime() for coordinated
replay session creation instead of making direct fetch calls. Selected
events highlight with timeline-event-selected class, and cross-module
selection is cleared between sidebar and full-page timeline views.
- timeline.js: use SpaxelReplay.jumpToTime() in handleSeek, export
clearSelection and hideNowReplayingChip, clear sidebar on jump
- sidebar-timeline.js: clear full-page timeline selection on sidebar jump
- replay.js: return promise from exitReplayMode for proper chaining
- timeline.test.js: 14 tests covering timestamp emission, highlighting,
now-replaying chip, cross-module coordination, simple mode gating,
and error handling
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add ?maxFps=N URL parameter to configure the frame rate cap at renderer
initialization time. ?maxFps=30 caps at 30fps (default for struggling
mobile devices), ?maxFps=60 disables the initial cap, and ?maxFps=0
disables both the cap and auto-detection. Also respects the
autoDetectStrugglingDevice config flag in the detection function.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
notifications.js was loaded as a classic <script> in simple.html but used
`export class`, causing "Unexpected token 'export'" and a blank page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>