fix: resolve remaining Go compilation errors across mqtt, analytics, localization, ingestion

- cmd/mothership/main.go: fix GetAllZones (single return), LastSeenAt vs LastSeenMs,
  remove undefined fusionEngine block, fix weights.GetLinkWeight usage, hoist
  learningHandler scope, remove unused recordingBuf/lastDetectionEvent vars, remove
  sync import, fix computeZoneQuality pointer dereference, fix pred field names
  (PredictedNextZoneID/PredictionConfidence), fix AccuracyStats.TotalPredictions,
  add GetNodeOfflineDuration to healthProviderAdapter, fix GetAccuracyDelta stub
- internal/api/guided.go: refactor GuidedManager interface to use time.Duration for
  TriggerNodeOffline, use any for zonesHandler/nodesHandler, remove diagnostics.Tooltip
  dependency, add GetTooltipAny type-assertion approach for cross-package tooltip access
- internal/api/tracks.go: unify TracksProvider to use signal.TrackedBlob directly via
  type alias to resolve interface mismatch
- internal/api/diurnal.go: add signalProcessorManagerAdapter and
  NewDiurnalHandlerFromSignal to bridge signal.ProcessorManager to DiurnalProcessorManager
- internal/guidedtroubleshoot/quality.go: add RecordEdit, MarkHintShown, GetTooltipAny
  methods to Manager to satisfy api interfaces
- internal/fusion/fusion.go: remove unused log import, fix oy declared-and-not-used

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-11 08:14:53 -04:00
parent ba4074e84d
commit bbb29a2629
6 changed files with 119 additions and 108 deletions

View file

@ -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.

View file

@ -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{

View file

@ -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,
})
}

View file

@ -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.

View file

@ -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

View file

@ -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
}