spaxel/mothership/internal/api/briefing.go
jedarden f99dc15a2d feat: complete crowd flow visualization implementation
- Fix Viz3D exports to include flow visualization functions
- Export setFlowLayerVisible, setDwellLayerVisible, setCorridorLayerVisible
- Export setFlowTimeFilter, setFlowData, setDwellData, setCorridorData
- Remove duplicate setDwellLayerVisible function definition

This completes the crowd flow visualization feature that was
already implemented in the backend (flow.go) and frontend
(crowdflow.js, viz3d.js) but had missing exports in the Viz3D module.
2026-04-11 07:27:21 -04:00

349 lines
10 KiB
Go

// Package api provides REST API handlers for morning briefings.
package api
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
_ "modernc.org/sqlite"
"github.com/spaxel/mothership/internal/briefing"
)
// BriefingHandler manages morning briefing REST endpoints.
type BriefingHandler struct {
generator *briefing.Generator
db *sql.DB
notifyService briefing.NotifyService
zoneProvider briefing.ZoneProvider
personProvider briefing.PersonProvider
predictionProvider briefing.PredictionProvider
healthProvider briefing.HealthProvider
}
// NewBriefingHandler creates a new briefing handler.
func NewBriefingHandler(dataDir string) (*BriefingHandler, error) {
gen, err := briefing.NewGenerator(dataDir + "/spaxel.db")
if err != nil {
return nil, err
}
// Open database connection for settings persistence
db, err := sql.Open("sqlite", dataDir+"/spaxel.db")
if err != nil {
gen.Close()
return nil, err
}
db.SetMaxOpenConns(1)
return &BriefingHandler{
generator: gen,
db: db,
}, nil
}
// SetProviders sets the provider interfaces for briefing generation.
func (h *BriefingHandler) SetProviders(z briefing.ZoneProvider, p briefing.PersonProvider, pr briefing.PredictionProvider, hp briefing.HealthProvider) {
h.zoneProvider = z
h.personProvider = p
h.predictionProvider = pr
h.healthProvider = hp
h.generator.SetProviders(z, p, pr, hp)
}
// Close closes the generator and database connection.
func (h *BriefingHandler) Close() error {
var firstErr error
if h.generator != nil {
if err := h.generator.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if h.db != nil {
if err := h.db.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
// SetNotifyService sets the notification service for sending test notifications.
func (h *BriefingHandler) SetNotifyService(notifySvc briefing.NotifyService) {
h.notifyService = notifySvc
}
// RegisterRoutes registers the briefing API routes.
func (h *BriefingHandler) RegisterRoutes(r chi.Router) {
r.Get("/api/briefing", h.handleGetBriefing)
r.Get("/api/briefing/today", h.handleGetTodayBriefing)
r.Get("/api/briefing/{date}", h.handleGetBriefingByDate)
r.Post("/api/briefing/generate", h.handleGenerateBriefing)
r.Get("/api/briefing/latest", h.handleGetLatestBriefing)
r.Post("/api/briefing/{id}/acknowledge", h.handleAcknowledgeBriefing)
r.Get("/api/briefing/settings", h.handleGetSettings)
r.Patch("/api/briefing/settings", h.handleUpdateSettings)
r.Post("/api/briefing/test", h.handleTestNotification)
}
// handleGetBriefing returns the briefing for today or a specific date.
func (h *BriefingHandler) handleGetBriefing(w http.ResponseWriter, r *http.Request) {
date := r.URL.Query().Get("date")
if date == "" {
date = time.Now().Format("2006-01-02")
}
person := r.URL.Query().Get("person")
b, err := h.generator.Get(date, person)
if err != nil {
http.Error(w, "Briefing not found", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, b)
}
// handleGetBriefingByDate returns the briefing for a specific date (RESTful path parameter).
func (h *BriefingHandler) handleGetBriefingByDate(w http.ResponseWriter, r *http.Request) {
date := chi.URLParam(r, "date")
if date == "" {
http.Error(w, "Date parameter required", http.StatusBadRequest)
return
}
person := r.URL.Query().Get("person")
b, err := h.generator.Get(date, person)
if err != nil {
http.Error(w, "Briefing not found", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, b)
}
// handleGenerateBriefing generates a new briefing for the given date.
func (h *BriefingHandler) handleGenerateBriefing(w http.ResponseWriter, r *http.Request) {
var req struct {
Date string `json:"date"`
Person string `json:"person"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if req.Date == "" {
req.Date = time.Now().Format("2006-01-02")
}
b, err := h.generator.Generate(req.Date, req.Person)
if err != nil {
log.Printf("[ERROR] Failed to generate briefing: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := h.generator.Save(b); err != nil {
log.Printf("[ERROR] Failed to save briefing: %v", err)
// Still return the briefing even if save failed
}
writeJSON(w, http.StatusOK, b)
}
// handleGetLatestBriefing returns the most recent briefing.
func (h *BriefingHandler) handleGetLatestBriefing(w http.ResponseWriter, r *http.Request) {
b, err := h.generator.GetLatest()
if err != nil {
http.Error(w, "No briefing found", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, b)
}
// handleGetSettings returns briefing settings.
func (h *BriefingHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
// Try to load settings from database
var settingsJSON sql.NullString
err := h.db.QueryRow("SELECT value_json FROM settings WHERE key = 'briefing_config'").Scan(&settingsJSON)
settings := map[string]interface{}{
"enabled": true,
"time": "07:00",
"push_notification": true,
"auto_generate": true,
}
if err == nil && settingsJSON.Valid {
var savedConfig map[string]interface{}
if err := json.Unmarshal([]byte(settingsJSON.String), &savedConfig); err == nil {
if enabled, ok := savedConfig["enabled"].(bool); ok {
settings["enabled"] = enabled
}
if timeStr, ok := savedConfig["time"].(string); ok {
settings["time"] = timeStr
}
if push, ok := savedConfig["push_notification"].(bool); ok {
settings["push_notification"] = push
}
if auto, ok := savedConfig["auto_generate"].(bool); ok {
settings["auto_generate"] = auto
}
}
}
writeJSON(w, http.StatusOK, settings)
}
// handleUpdateSettings updates briefing settings.
func (h *BriefingHandler) handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
var settings map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// Validate settings
if timeStr, ok := settings["time"].(string); ok {
// Validate time format (HH:MM)
var h, m int
_, err := fmt.Sscanf(timeStr, "%d:%d", &h, &m)
if err != nil || h < 0 || h > 23 || m < 0 || m > 59 {
http.Error(w, "Invalid time format, use HH:MM", http.StatusBadRequest)
return
}
}
// Save to database settings table
settingsJSON, err := json.Marshal(settings)
if err != nil {
log.Printf("[ERROR] Failed to marshal briefing settings: %v", err)
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
return
}
_, err = h.db.Exec(`
INSERT OR REPLACE INTO settings (key, value_json, updated_at)
VALUES ('briefing_config', ?, strftime('%s', 'now') * 1000)
`, string(settingsJSON))
if err != nil {
log.Printf("[ERROR] Failed to save briefing settings: %v", err)
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
return
}
log.Printf("[INFO] Briefing settings updated: %+v", settings)
// Update scheduler config if available
// Note: The scheduler will pick up the new config on next check
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// handleTestNotification sends a test briefing notification.
func (h *BriefingHandler) handleTestNotification(w http.ResponseWriter, r *http.Request) {
// Generate a test briefing for today
date := time.Now().Format("2006-01-02")
b, err := h.generator.Generate(date, "")
if err != nil {
log.Printf("[ERROR] Failed to generate test briefing: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Send via notification service if available
if h.notifyService != nil {
notif := briefing.Notification{
Title: "Morning Briefing (Test)",
Body: b.Content,
Priority: 1,
Tags: []string{"briefing", "test"},
Timestamp: time.Now(),
}
if err := h.notifyService.Send(notif); err != nil {
log.Printf("[ERROR] Failed to send test notification: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"status": "error",
"error": err.Error(),
"briefing": b,
})
return
}
log.Printf("[INFO] Test briefing notification sent")
} else {
log.Printf("[INFO] Test briefing notification (no notify service): %s", b.Content)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "sent",
"briefing": b,
})
}
// handleGetTodayBriefing returns today's briefing, generating if needed.
func (h *BriefingHandler) handleGetTodayBriefing(w http.ResponseWriter, r *http.Request) {
today := time.Now().Format("2006-01-02")
person := r.URL.Query().Get("person")
// First try to get existing briefing
b, err := h.generator.Get(today, person)
if err == nil {
// Check if it's marked as delivered
if !b.Delivered {
// Mark as delivered on first fetch
if err := h.generator.MarkDelivered(b.ID); err != nil {
log.Printf("[WARN] Failed to mark briefing as delivered: %v", err)
}
b.Delivered = true
}
writeJSON(w, http.StatusOK, b)
return
}
// No briefing exists, generate one
b, err = h.generator.Generate(today, person)
if err != nil {
log.Printf("[ERROR] Failed to generate today's briefing: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Save the new briefing
if err := h.generator.Save(b); err != nil {
log.Printf("[ERROR] Failed to save briefing: %v", err)
}
writeJSON(w, http.StatusOK, b)
}
// handleAcknowledgeBriefing marks a briefing as acknowledged by the user.
func (h *BriefingHandler) handleAcknowledgeBriefing(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
http.Error(w, "Briefing ID required", http.StatusBadRequest)
return
}
// Mark as acknowledged
if err := h.generator.MarkAcknowledged(id); err != nil {
log.Printf("[ERROR] Failed to acknowledge briefing: %v", err)
http.Error(w, "Failed to acknowledge briefing", http.StatusInternalServerError)
return
}
log.Printf("[INFO] Briefing %s acknowledged", id)
writeJSON(w, http.StatusOK, map[string]string{"status": "acknowledged"})
}
// GetGenerator returns the underlying briefing generator.
func (h *BriefingHandler) GetGenerator() *briefing.Generator {
return h.generator
}