spaxel/mothership/internal/api/status.go
jedarden 6d8fc086ff api: register GET /api/status and GET /api/occupancy endpoints
Implements two REST API endpoints as defined in the plan's REST API spec:

1. GET /api/status - Returns system status including:
   - version: Application version string
   - nodes: Number of online nodes
   - blobs: Number of currently tracked blobs
   - uptime_s: Uptime in seconds
   - detection_quality: System-wide detection quality (0-100)

2. GET /api/occupancy - Returns zone occupancy data:
   - zones: Map of zone names to {count, people[]}
     - count: Number of people in the zone
     - people: List of person names (BLE-identified)

These are simple read-only endpoints for dashboard and Home Assistant
integration for quick system checks and occupancy queries without WebSocket.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 13:39:56 -04:00

151 lines
3.9 KiB
Go

// Package api provides REST API handlers for Spaxel system status and occupancy.
package api
import (
"net/http"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/spaxel/mothership/internal/signal"
"github.com/spaxel/mothership/internal/zones"
)
// StatusHandler handles GET /api/status and GET /api/occupancy.
type StatusHandler struct {
mu sync.RWMutex
pm ProcessorManagerProvider
zonesMgr ZonesManagerProvider
startTime time.Time
getNodeCount func() int
}
// ProcessorManagerProvider provides access to signal processor data.
type ProcessorManagerProvider interface {
GetSystemHealth() float64
GetTrackedBlobs() []signal.TrackedBlob
}
// ZonesManagerProvider provides access to zone data.
type ZonesManagerProvider interface {
GetAllZones() []*zones.Zone
GetZoneOccupancy(zoneID string) *zones.ZoneOccupancy
}
// NewStatusHandler creates a new status handler.
func NewStatusHandler(startTime time.Time, getNodeCount func() int) *StatusHandler {
return &StatusHandler{
startTime: startTime,
getNodeCount: getNodeCount,
}
}
// SetProcessorManager sets the signal processor manager.
func (h *StatusHandler) SetProcessorManager(pm ProcessorManagerProvider) {
h.mu.Lock()
defer h.mu.Unlock()
h.pm = pm
}
// SetZonesManager sets the zones manager.
func (h *StatusHandler) SetZonesManager(zm ZonesManagerProvider) {
h.mu.Lock()
defer h.mu.Unlock()
h.zonesMgr = zm
}
// RegisterRoutes registers status and occupancy endpoints.
func (h *StatusHandler) RegisterRoutes(r chi.Router) {
r.Get("/api/status", h.getStatus)
r.Get("/api/occupancy", h.getOccupancy)
}
// getStatus handles GET /api/status.
//
// Returns:
// - version: Application version string
// - nodes: Number of online nodes
// - blobs: Number of currently tracked blobs
// - uptime_s: Uptime in seconds
// - detection_quality: System-wide detection quality (0-100)
func (h *StatusHandler) getStatus(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
defer h.mu.RUnlock()
// Get node count
nodes := 0
if h.getNodeCount != nil {
nodes = h.getNodeCount()
}
// Get blob count
blobs := 0
if h.pm != nil {
blobs = len(h.pm.GetTrackedBlobs())
}
// Get uptime
uptime := int64(time.Since(h.startTime).Seconds())
// Get detection quality (0-100 scale)
quality := 0.0
if h.pm != nil {
// GetSystemHealth returns 0-1, convert to 0-100
quality = h.pm.GetSystemHealth() * 100
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"version": "1.0.0", // TODO: get from build info
"nodes": nodes,
"blobs": blobs,
"uptime_s": uptime,
"detection_quality": int(quality), // Convert to int for cleaner JSON
})
}
// occupancyResponse represents the occupancy data for a single zone.
type occupancyResponse struct {
Count int `json:"count"`
People []string `json:"people"`
}
// getOccupancy handles GET /api/occupancy.
//
// Returns a map of zone names to their current occupancy:
// - count: Number of people in the zone
// - people: List of person names (BLE-identified) in the zone
func (h *StatusHandler) getOccupancy(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
defer h.mu.RUnlock()
result := make(map[string]occupancyResponse)
if h.zonesMgr == nil {
writeJSON(w, http.StatusOK, result)
return
}
// Get all zones
allZones := h.zonesMgr.GetAllZones()
for _, z := range allZones {
occ := h.zonesMgr.GetZoneOccupancy(z.ID)
if occ == nil {
result[z.Name] = occupancyResponse{
Count: 0,
People: []string{},
}
continue
}
// Convert blob IDs to person labels if we have a processor manager
// For now, return empty people list - identity resolution requires
// additional provider interface
result[z.Name] = occupancyResponse{
Count: occ.Count,
People: []string{}, // TODO: resolve blob IDs to person labels
}
}
writeJSON(w, http.StatusOK, result)
}