Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Ran gofmt across the entire mothership codebase to ensure consistent code formatting per Go standards. All tests pass after formatting.
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
// Package api provides REST API handlers for crowd flow analytics.
|
|
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
_ "modernc.org/sqlite"
|
|
|
|
"github.com/spaxel/mothership/internal/analytics"
|
|
)
|
|
|
|
// AnalyticsHandler manages the crowd flow analytics API endpoints.
|
|
type AnalyticsHandler struct {
|
|
flowAccumulator *analytics.FlowAccumulator
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewAnalyticsHandler creates a new analytics handler.
|
|
func NewAnalyticsHandler(db *sql.DB, cellSizeM float64) *AnalyticsHandler {
|
|
flowAcc := analytics.NewFlowAccumulator(db, cellSizeM)
|
|
if err := flowAcc.InitSchema(); err != nil {
|
|
log.Printf("[WARN] Failed to initialize analytics schema: %v", err)
|
|
}
|
|
|
|
// Start background prune job
|
|
go func() {
|
|
ticker := time.NewTicker(24 * time.Hour)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
if err := flowAcc.PruneOldData(); err != nil {
|
|
log.Printf("[WARN] Failed to prune old analytics data: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &AnalyticsHandler{
|
|
flowAccumulator: flowAcc,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// GetFlowAccumulator returns the flow accumulator for use by other packages.
|
|
func (h *AnalyticsHandler) GetFlowAccumulator() *analytics.FlowAccumulator {
|
|
return h.flowAccumulator
|
|
}
|
|
|
|
// RegisterRoutes registers analytics endpoints.
|
|
func (h *AnalyticsHandler) RegisterRoutes(r chi.Router) {
|
|
r.Get("/api/analytics/flow", h.getFlowMap)
|
|
r.Get("/api/analytics/dwell", h.getDwellHeatmap)
|
|
r.Get("/api/analytics/corridors", h.getCorridors)
|
|
}
|
|
|
|
// getFlowMap handles GET /api/analytics/flow
|
|
// Query params: person_id (optional), since (ISO8601), until (ISO8601)
|
|
func (h *AnalyticsHandler) getFlowMap(w http.ResponseWriter, r *http.Request) {
|
|
// Parse query parameters
|
|
var personID *string
|
|
if pid := r.URL.Query().Get("person_id"); pid != "" {
|
|
personID = &pid
|
|
}
|
|
|
|
var since, until *time.Time
|
|
if s := r.URL.Query().Get("since"); s != "" {
|
|
t, err := time.Parse(time.RFC3339, s)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid since timestamp")
|
|
return
|
|
}
|
|
since = &t
|
|
}
|
|
|
|
if u := r.URL.Query().Get("until"); u != "" {
|
|
t, err := time.Parse(time.RFC3339, u)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid until timestamp")
|
|
return
|
|
}
|
|
until = &t
|
|
}
|
|
|
|
flowMap, err := h.flowAccumulator.ComputeFlowMap(personID, since, until)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "failed to compute flow map")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, flowMap)
|
|
}
|
|
|
|
// getDwellHeatmap handles GET /api/analytics/dwell
|
|
// Query params: person_id (optional)
|
|
func (h *AnalyticsHandler) getDwellHeatmap(w http.ResponseWriter, r *http.Request) {
|
|
var personID *string
|
|
if pid := r.URL.Query().Get("person_id"); pid != "" {
|
|
personID = &pid
|
|
}
|
|
|
|
heatmap, err := h.flowAccumulator.ComputeDwellHeatmap(personID)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "failed to compute dwell heatmap")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, heatmap)
|
|
}
|
|
|
|
// getCorridors handles GET /api/analytics/corridors
|
|
func (h *AnalyticsHandler) getCorridors(w http.ResponseWriter, r *http.Request) {
|
|
corridors, err := h.flowAccumulator.GetCorridors()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, "failed to get corridors")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"corridors": corridors,
|
|
})
|
|
}
|