// NextNVR v0.1.0 — HTTP server and routing // Serves the embedded SPA and API endpoints. package main import ( "embed" "encoding/json" "io/fs" "log" "net/http" "strings" ) //go:embed public/* var publicFiles embed.FS // Server wraps the HTTP server and application state. type Server struct { config Config http *http.Server mux *http.ServeMux appConfig *Config // mutable config reference for hot-reload } // NewServer creates and configures the HTTP server. func NewServer(cfg Config) (*Server, error) { s := &Server{ config: cfg, appConfig: &cfg, mux: http.NewServeMux(), } s.registerRoutes() s.http = &http.Server{ Addr: cfg.Server.BindHost + cfg.Server.Port, Handler: s.middleware(s.mux), } return s, nil } // ListenAndServe starts the HTTP server. func (s *Server) ListenAndServe() error { log.Printf("HTTP server listening on %s", s.http.Addr) return s.http.ListenAndServe() } // Close shuts down the HTTP server. func (s *Server) Close() { if s.http != nil { s.http.Close() } } // registerRoutes sets up all API and static file routes. func (s *Server) registerRoutes() { // API endpoints. s.mux.HandleFunc("/api/cameras", s.handleCameras) s.mux.HandleFunc("/api/cameras/", s.handleCameraByID) s.mux.HandleFunc("/api/config", s.handleConfig) s.mux.HandleFunc("/api/scan", s.handleScan) s.mux.HandleFunc("/api/status", s.handleStatus) s.mux.HandleFunc("/api/recordings", s.handleRecordings) // Static file server for recordings (actual video files on disk). s.mux.Handle("/recordings/", http.StripPrefix("/recordings/", http.FileServer(http.Dir(s.config.Storage.RecordingsPath)))) // Static file server for embedded SPA. publicFS, _ := fs.Sub(publicFiles, "public") s.mux.Handle("/", http.FileServer(http.FS(publicFS))) } // middleware wraps handlers with CORS and logging. func (s *Server) middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // API logging. if strings.HasPrefix(r.URL.Path, "/api/") { log.Printf("%s %s", r.Method, r.URL.Path) } next.ServeHTTP(w, r) }) } // jsonResponse writes a JSON response with the given status code. func jsonResponse(w http.ResponseWriter, status int, resp APIResponse) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(resp) }