spaxel/mothership/internal/api/tracks.go
jedarden a97960bf67 fix: resolve analytics API test failures and improve corridor response format
- Fix TestAnalyticsHandler_ErrorHandling to use proper in-memory database
  instead of nil database which caused nil pointer dereference
- Update handleGetCorridors to return corridors wrapped in {corridors: [...]}
  for consistency with frontend expectations from crowdflow.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:34:24 -04:00

120 lines
3.9 KiB
Go

// Package api provides REST API handlers for Spaxel tracks (tracked people).
package api
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/spaxel/mothership/internal/signal"
)
// Track represents a tracked person with identity and position.
type Track struct {
ID int `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
VX float64 `json:"vx"`
VY float64 `json:"vy"`
VZ float64 `json:"vz"`
Weight float64 `json:"weight"`
PersonID string `json:"person_id,omitempty"`
PersonLabel string `json:"person_label,omitempty"`
PersonColor string `json:"person_color,omitempty"`
IdentityConfidence float64 `json:"identity_confidence,omitempty"`
IdentitySource string `json:"identity_source,omitempty"`
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() []signal.TrackedBlob
}
// TracksHandler manages the tracks REST API.
type TracksHandler struct {
provider TracksProvider
}
// NewTracksHandler creates a new tracks handler.
func NewTracksHandler(provider TracksProvider) *TracksHandler {
return &TracksHandler{provider: provider}
}
// signalProcessorTracksAdapter wraps *signal.ProcessorManager to implement TracksProvider.
type signalProcessorTracksAdapter struct {
pm interface {
GetTrackedBlobs() []signal.TrackedBlob
}
}
func (a *signalProcessorTracksAdapter) GetTrackedBlobs() []TrackedBlob {
return a.pm.GetTrackedBlobs()
}
// NewTracksHandlerFromSignal creates a TracksHandler from a *signal.ProcessorManager.
func NewTracksHandlerFromSignal(pm interface {
GetTrackedBlobs() []signal.TrackedBlob
}) *TracksHandler {
return &TracksHandler{
provider: &signalProcessorTracksAdapter{pm: pm},
}
}
// RegisterRoutes mounts tracks endpoints on r.
//
// GET /api/tracks
//
// @Summary List tracked people
// @Description Returns all currently tracked people with identity information and position. Identity is populated by BLE-to-blob matching when BLE devices are associated with people.
// @Tags tracks
// @Produce json
// @Success 200 {array} Track "List of tracks with identity fields"
// @Router /api/tracks [get]
func (h *TracksHandler) RegisterRoutes(r chi.Router) {
r.Get("/api/tracks", h.listTracks)
}
// listTracks handles GET /api/tracks.
//
// Returns all currently tracked people with identity information and position.
// The response includes:
// - id: Blob ID
// - x, y, z: Position coordinates (meters)
// - vx, vy, vz: Velocity vectors (m/s)
// - weight: Blob weight (confidence)
// - person_id: UUID of associated person (if matched)
// - person_label: Human-readable name (if matched)
// - person_color: Display color for person (if matched)
// - identity_confidence: BLE-to-blob match confidence (0-1)
// - identity_source: Source of identity ("ble", "vision", etc.)
// - posture: Detected posture (standing, sitting, etc.)
//
// Status codes:
// - 200: Success
func (h *TracksHandler) listTracks(w http.ResponseWriter, r *http.Request) {
blobs := h.provider.GetTrackedBlobs()
tracks := make([]Track, len(blobs))
for i, b := range blobs {
tracks[i] = Track{
ID: b.ID,
X: b.X,
Y: b.Y,
Z: b.Z,
VX: b.VX,
VY: b.VY,
VZ: b.VZ,
Weight: b.Weight,
PersonID: b.PersonID,
PersonLabel: b.PersonLabel,
PersonColor: b.PersonColor,
IdentityConfidence: b.IdentityConfidence,
IdentitySource: b.IdentitySource,
Posture: b.Posture,
}
}
writeJSON(w, http.StatusOK, tracks)
}