// Package api wires the HTTP routes: the device ingest endpoint, the dashboard // WebSocket, the read APIs the dashboard polls, and the dashboard page itself. package api import ( "encoding/json" "fmt" "html/template" "io/fs" "log/slog" "net/http" "gpsbackend/internal/config" "gpsbackend/internal/hub" "gpsbackend/internal/store" ) // Server holds the dependencies shared by every handler. type Server struct { cfg config.Config store *store.Store hub *hub.Hub tmpl *template.Template static http.Handler } // NewServer builds a Server. assets must contain "templates/" and "static/" // directories at its root (an embed.FS in production, os.DirFS in -dev mode). func NewServer(cfg config.Config, st *store.Store, h *hub.Hub, assets fs.FS) (*Server, error) { tmpl, err := template.ParseFS(assets, "templates/*.html") if err != nil { return nil, fmt.Errorf("parse templates: %w", err) } staticFS, err := fs.Sub(assets, "static") if err != nil { return nil, fmt.Errorf("sub static FS: %w", err) } return &Server{ cfg: cfg, store: st, hub: h, tmpl: tmpl, static: http.FileServer(http.FS(staticFS)), }, nil } // Handler returns the fully-routed http.Handler with middleware applied. func (s *Server) Handler() http.Handler { mux := http.NewServeMux() // Device ingest. mux.HandleFunc("POST /api/locations", s.handlePostLocations) // Devices. mux.HandleFunc("GET /api/devices", s.handleListDevices) mux.HandleFunc("GET /api/devices/{id}", s.handleGetDevice) mux.HandleFunc("PUT /api/devices/{id}", s.handleUpdateDevice) mux.HandleFunc("GET /api/devices/{id}/track", s.handleGetTrack) mux.HandleFunc("GET /api/devices/{id}/export.csv", s.handleExportCSV) // Settings. mux.HandleFunc("GET /api/settings", s.handleGetSettings) mux.HandleFunc("PUT /api/settings", s.handlePutSettings) // Alerts. mux.HandleFunc("GET /api/alerts", s.handleListAlerts) mux.HandleFunc("POST /api/alerts/{id}/ack", s.handleAckAlert) // Geofences. mux.HandleFunc("GET /api/geofences", s.handleListGeofences) mux.HandleFunc("POST /api/geofences", s.handleCreateGeofence) mux.HandleFunc("DELETE /api/geofences/{id}", s.handleDeleteGeofence) // Live dashboard WebSocket. mux.HandleFunc("GET /ws/dashboard", s.handleDashboardWS) // Health probe. mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) // Static assets and the dashboard page. mux.Handle("GET /static/", http.StripPrefix("/static/", s.static)) mux.HandleFunc("GET /{$}", s.handleDashboardPage) return withMiddleware(mux, s.cfg) } // writeJSON marshals v and writes it with the given status code. func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("encode json response", "err", err) } } // writeError writes a {"error": "..."} JSON body with the given status code. func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }