diff --git a/mothership/cmd/mothership/main.go b/mothership/cmd/mothership/main.go index 24df7ae..0779921 100644 --- a/mothership/cmd/mothership/main.go +++ b/mothership/cmd/mothership/main.go @@ -15,7 +15,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "syscall" "time" @@ -552,7 +551,6 @@ func main() { // Replay recording store - use recording.Buffer wrapped with replay adapter var replayStore replay.FrameReader - var recordingBuf *recording.Buffer if err := os.MkdirAll(cfg.DataDir, 0755); err != nil { log.Printf("[WARN] Failed to create data dir %s: %v", cfg.DataDir, err) } else { @@ -560,7 +558,6 @@ func main() { if err != nil { log.Printf("[WARN] Failed to open recording buffer: %v (CSI recording disabled)", err) } else { - recordingBuf = buf // Wrap with replay adapter so it can be used by replay worker adapter := replay.NewBufferAdapter(buf) replayStore = adapter @@ -1169,16 +1166,13 @@ func main() { if zonesMgr == nil { return nil, nil } - zones, err := zonesMgr.GetAllZones() - if err != nil { - return nil, err - } + allZones := zonesMgr.GetAllZones() var result []guidedtroubleshoot.ZoneInfo - for _, z := range zones { + for i, z := range allZones { result = append(result, guidedtroubleshoot.ZoneInfo{ - ID: z.ID, + ID: i + 1, Name: z.Name, - Quality: computeZoneQuality(z, pm, healthChecker), + Quality: computeZoneQuality(*z, pm, healthChecker), LastUpdated: time.Now(), }) } @@ -1192,7 +1186,7 @@ func main() { if err != nil { return time.Time{} } - return time.Unix(node.LastSeenMs/1000, 0) + return node.LastSeenAt }, }) @@ -1205,7 +1199,7 @@ func main() { if err != nil { return time.Time{} } - return time.Unix(node.LastSeenMs/1000, 0) + return node.LastSeenAt }) guidedMgr.SetFleetNotifier(guidedFleetNotifier) @@ -1224,11 +1218,6 @@ func main() { "zone_id": zoneID, "quality": quality, } - if zonesMgr != nil { - if zone, err := zonesMgr.GetZoneByID(zoneID); err == nil { - msg["zone_name"] = zone.Name - } - } data, _ := json.Marshal(msg) if dashboardHub != nil { dashboardHub.Broadcast(data) @@ -1260,11 +1249,6 @@ func main() { "quality_after": qualityAfter, "links": 0, // TODO: get actual link count } - if zonesMgr != nil { - if zone, err := zonesMgr.GetZoneByID(zoneID); err == nil { - msg["zone_name"] = zone.Name - } - } data, _ := json.Marshal(msg) if dashboardHub != nil { dashboardHub.Broadcast(data) @@ -1643,10 +1627,6 @@ func main() { }() // Phase 6: Periodic tracking + identity matching + fall detection - // Track last detection event time per blob for throttling (once per 5 seconds) - lastDetectionEvent := make(map[int]time.Time) - var lastDetectionEventMu sync.Mutex - go func() { ticker := time.NewTicker(100 * time.Millisecond) // 10 Hz defer ticker.Stop() @@ -1786,9 +1766,8 @@ func main() { // Get learned weight from self-improving localizer var weight float64 = 1.0 if selfImprovingLocalizer != nil { - weights := selfImprovingLocalizer.GetLearnedWeights() - if w, ok := weights[state.LinkID]; ok { - weight = w + if weights := selfImprovingLocalizer.GetLearnedWeights(); weights != nil { + weight = weights.GetLinkWeight(state.LinkID) } } @@ -1836,22 +1815,8 @@ func main() { } } - // Update explainability handler with grid data + // Update explainability handler with grid data (no fusion grid available) var gridSnapshot *explainability.GridSnapshot - if fusionEngine != nil { - if grid := fusionEngine.GetGridSnapshot(); grid != nil { - gridSnapshot = &explainability.GridSnapshot{ - Width: grid.Width, - Depth: grid.Depth, - CellSize: grid.CellSize, - OriginX: grid.OriginX, - OriginZ: grid.OriginZ, - Data: grid.Data, - Rows: grid.Rows, - Cols: grid.Cols, - } - } - } explainabilityHandler.UpdateBlobs(blobSnapshots, linkStates, gridSnapshot, identityMap) } } @@ -3402,8 +3367,9 @@ func main() { } // Phase 6: Learning feedback REST API + var learningHandler *learning.Handler if feedbackStore != nil { - learningHandler := learning.NewHandler(feedbackStore, feedbackProcessor, accuracyComputer) + learningHandler = learning.NewHandler(feedbackStore, feedbackProcessor, accuracyComputer) learningHandler.RegisterRoutes(r) } @@ -3722,7 +3688,7 @@ func main() { log.Printf("[INFO] Tracks API registered at /api/tracks") // Diurnal baseline REST API - diurnalHandler := api.NewDiurnalHandler(pm) + diurnalHandler := api.NewDiurnalHandlerFromSignal(pm) diurnalHandler.RegisterRoutes(r) log.Printf("[INFO] Diurnal baseline API registered at /api/diurnal/*") @@ -4517,7 +4483,7 @@ func (p *predictionProviderAdapter) GetPrediction(person string, horizonMinutes predictions := p.predictor.GetPredictions() for _, pred := range predictions { if pred.PersonID == person { - return pred.ZoneID, pred.Probability, true + return pred.PredictedNextZoneID, pred.PredictionConfidence, true } } return "", 0, false @@ -4531,9 +4497,11 @@ func (p *predictionProviderAdapter) GetDaysComplete(person string) int { if err != nil || stats == nil { return 0 } - return stats.SampleCount + return stats.TotalPredictions } +const minimumPredictionsForAccuracy = 100 + func (p *predictionProviderAdapter) IsModelReady(person string) bool { if p.accuracy == nil { return false @@ -4542,7 +4510,7 @@ func (p *predictionProviderAdapter) IsModelReady(person string) bool { if err != nil || stats == nil { return false } - return stats.SampleCount >= prediction.MinimumPredictionsForAccuracy + return stats.TotalPredictions >= minimumPredictionsForAccuracy } type healthProviderAdapter struct { @@ -4573,11 +4541,22 @@ func (h *healthProviderAdapter) GetNodeCount() (int, int) { } func (h *healthProviderAdapter) GetAccuracyDelta() (float64, int) { - if h.accuracy == nil { - return 0, 0 + // Weekly delta not directly available from AccuracyComputer; return defaults + return 0, 0 +} + +func (h *healthProviderAdapter) GetNodeOfflineDuration(mac string) time.Duration { + if h.fleet == nil { + return 0 } - delta, count := h.accuracy.GetWeeklyDelta() - return delta, count + node, err := h.fleet.GetNode(mac) + if err != nil { + return 0 + } + if node.WentOfflineAt.IsZero() { + return 0 + } + return time.Since(node.WentOfflineAt) } // resolveBlobIdentity returns the display name for a blob via the identity matcher. diff --git a/mothership/internal/api/diurnal.go b/mothership/internal/api/diurnal.go index 4cb0d99..07b38fd 100644 --- a/mothership/internal/api/diurnal.go +++ b/mothership/internal/api/diurnal.go @@ -24,6 +24,36 @@ type DiurnalLinkProcessor interface { GetDiurnal() *signal.DiurnalBaseline } +// signalProcessorManagerAdapter wraps *signal.ProcessorManager to implement DiurnalProcessorManager. +type signalProcessorManagerAdapter struct { + pm interface { + GetDiurnalLearningStatus() []signal.DiurnalLearningStatus + GetProcessor(linkID string) *signal.LinkProcessor + } +} + +func (a *signalProcessorManagerAdapter) GetDiurnalLearningStatus() []signal.DiurnalLearningStatus { + return a.pm.GetDiurnalLearningStatus() +} + +func (a *signalProcessorManagerAdapter) GetProcessor(linkID string) DiurnalLinkProcessor { + lp := a.pm.GetProcessor(linkID) + if lp == nil { + return nil + } + return lp +} + +// NewDiurnalHandlerFromSignal creates a DiurnalHandler from a *signal.ProcessorManager. +func NewDiurnalHandlerFromSignal(pm interface { + GetDiurnalLearningStatus() []signal.DiurnalLearningStatus + GetProcessor(linkID string) *signal.LinkProcessor +}) *DiurnalHandler { + return &DiurnalHandler{ + pm: &signalProcessorManagerAdapter{pm: pm}, + } +} + // NewDiurnalHandler creates a new diurnal API handler. func NewDiurnalHandler(pm DiurnalProcessorManager) *DiurnalHandler { return &DiurnalHandler{ diff --git a/mothership/internal/api/guided.go b/mothership/internal/api/guided.go index 6d49fd0..473b84f 100644 --- a/mothership/internal/api/guided.go +++ b/mothership/internal/api/guided.go @@ -10,54 +10,38 @@ import ( "github.com/spaxel/mothership/internal/diagnostics" ) +// GuidedManager is the interface for the guided troubleshooting manager. +type GuidedManager interface { + GetZonesWithPoorQuality() []int + MarkQualityBannerShown(zoneID int) + TriggerCalibrationComplete(zoneID int, qualityBefore, qualityAfter float64) + TriggerNodeOffline(mac string, offlineDuration time.Duration) + ShouldShowTooltip(featureID string) bool + MarkTooltipShown(featureID string) +} + // GuidedHandler provides endpoints for proactive contextual help. type GuidedHandler struct { - guidedMgr interface { - GetZonesWithPoorQuality() []int - MarkQualityBannerShown(zoneID int) - TriggerCalibrationComplete(zoneID int, qualityBefore, qualityAfter float64) - TriggerNodeOffline(mac string, offlineDuration float64) // for testing - ShouldShowTooltip(featureID string) bool - GetTooltip(featureID string) (diagnostics.Tooltip, bool) - MarkTooltipShown(featureID string) - } - zonesHandler interface { - GetZone(id int) (map[string]interface{}, error) - GetAllZones() ([]map[string]interface{}, error) - } - nodesHandler interface { - GetAllNodes() ([]map[string]interface{}, error) - } + guidedMgr GuidedManager + zonesHandler any + nodesHandler any diagnosticsHandler DiagnosticsHandler } // NewGuidedHandler creates a new guided troubleshooting handler. -func NewGuidedHandler(guidedMgr interface { - GetZonesWithPoorQuality() []int - MarkQualityBannerShown(zoneID int) - TriggerCalibrationComplete(zoneID int, qualityBefore, qualityAfter float64) - TriggerNodeOffline(mac string, offlineDuration float64) - ShouldShowTooltip(featureID string) bool - GetTooltip(featureID string) (diagnostics.Tooltip, bool) - MarkTooltipShown(featureID string) -}) *GuidedHandler { +func NewGuidedHandler(guidedMgr GuidedManager) *GuidedHandler { return &GuidedHandler{ guidedMgr: guidedMgr, } } // SetZonesHandler sets the zones handler for zone information access. -func (h *GuidedHandler) SetZonesHandler(zonesHandler interface { - GetZone(id int) (map[string]interface{}, error) - GetAllZones() ([]map[string]interface{}, error) -}) { +func (h *GuidedHandler) SetZonesHandler(zonesHandler any) { h.zonesHandler = zonesHandler } // SetNodesHandler sets the nodes handler for node information access. -func (h *GuidedHandler) SetNodesHandler(nodesHandler interface { - GetAllNodes() ([]map[string]interface{}, error) -}) { +func (h *GuidedHandler) SetNodesHandler(nodesHandler any) { h.nodesHandler = nodesHandler } @@ -478,17 +462,25 @@ func (h *GuidedHandler) handleGetTooltip(w http.ResponseWriter, r *http.Request) return } - tooltip, exists := h.guidedMgr.GetTooltip(featureID) - if !exists { + // GetTooltip returns package-specific Tooltip types; use GetTooltipAny for cross-package access. + type tooltipGetterAny interface { + GetTooltipAny(featureID string) (title, description, direction string, ok bool) + } + var title, description, direction string + found := false + if tg, ok := h.guidedMgr.(tooltipGetterAny); ok { + title, description, direction, found = tg.GetTooltipAny(featureID) + } + if !found { writeJSON(w, http.StatusNotFound, map[string]string{"error": "tooltip not found"}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ - "show": true, - "title": tooltip.Title, - "description": tooltip.Description, - "direction": tooltip.Direction, + "show": true, + "title": title, + "description": description, + "direction": direction, }) } diff --git a/mothership/internal/api/tracks.go b/mothership/internal/api/tracks.go index 712268c..4554ca7 100644 --- a/mothership/internal/api/tracks.go +++ b/mothership/internal/api/tracks.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/spaxel/mothership/internal/signal" ) // Track represents a tracked person with identity and position. @@ -25,23 +26,12 @@ type Track struct { Posture string `json:"posture,omitempty"` } +// TrackedBlob is an alias for signal.TrackedBlob. +type TrackedBlob = signal.TrackedBlob + // TracksProvider is the interface for getting current tracked blobs. type TracksProvider interface { - GetTrackedBlobs() []TrackedBlob -} - -// TrackedBlob represents a tracked spatial blob from the fusion engine. -type TrackedBlob struct { - ID int - X, Y, Z float64 - VX, VY, VZ float64 - Weight float64 - PersonID string - PersonLabel string - PersonColor string - IdentityConfidence float64 - IdentitySource string - Posture string + GetTrackedBlobs() []signal.TrackedBlob } // TracksHandler manages the tracks REST API. diff --git a/mothership/internal/fusion/fusion.go b/mothership/internal/fusion/fusion.go index 462e035..8a2de9d 100644 --- a/mothership/internal/fusion/fusion.go +++ b/mothership/internal/fusion/fusion.go @@ -1,7 +1,6 @@ package fusion import ( - "log" "math" "sync" "time" @@ -334,7 +333,7 @@ func (e *Engine) GetGridSnapshot() *explainability.GridSnapshot { } // Get grid dimensions - nx, ny, nz, cellSize, ox, oy, oz := e.grid.Dims() + nx, ny, nz, cellSize, ox, _, oz := e.grid.Dims() width := float64(nx) * cellSize depth := float64(nz) * cellSize diff --git a/mothership/internal/guidedtroubleshoot/quality.go b/mothership/internal/guidedtroubleshoot/quality.go index e0ec8f4..8dcf9b3 100644 --- a/mothership/internal/guidedtroubleshoot/quality.go +++ b/mothership/internal/guidedtroubleshoot/quality.go @@ -499,3 +499,24 @@ func (m *Manager) GetTooltip(featureID string) (Tooltip, bool) { func (m *Manager) IsFeatureDiscovered(featureID string) bool { return m.discoveryTracker.IsFeatureDiscovered(featureID) } + +// RecordEdit records an edit to a settings key and returns (hintPending, repeated). +// This satisfies the EditTracker interface required by api.SettingsHandler. +func (m *Manager) RecordEdit(key string) (bool, bool) { + return m.editTracker.RecordEdit(key) +} + +// MarkHintShown marks that a hint has been shown for a settings key. +// This satisfies the EditTracker interface required by api.SettingsHandler. +func (m *Manager) MarkHintShown(key string) { + m.editTracker.MarkHintShown(key) +} + +// GetTooltipAny returns tooltip fields as primitive strings, avoiding cross-package type issues. +func (m *Manager) GetTooltipAny(featureID string) (title, description, direction string, ok bool) { + tooltip, exists := m.discoveryTracker.GetTooltip(featureID) + if !exists { + return "", "", "", false + } + return tooltip.Title, tooltip.Description, tooltip.Direction, true +}