// Package main is the entry point for the RemoteRig central hub. package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/cubecraft/remoterig/internal/api" "github.com/cubecraft/remoterig/internal/auth" "github.com/cubecraft/remoterig/internal/db" "github.com/cubecraft/remoterig/internal/events" "github.com/cubecraft/remoterig/internal/mqtt" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "gopkg.in/yaml.v3" ) // Config holds the application configuration. type Config struct { DBPath string `yaml:"db_path"` APIKey string `yaml:"api_key"` Port string `yaml:"port"` ReadTimeout time.Duration `yaml:"read_timeout"` WriteTimeout time.Duration `yaml:"write_timeout"` IdleTimeout time.Duration `yaml:"idle_timeout"` MQTT struct { Broker string `yaml:"broker"` ClientID string `yaml:"client_id"` } `yaml:"mqtt"` Platform struct { Type string `yaml:"type"` MaxCameras int `yaml:"max_cameras"` } `yaml:"platform"` } func main() { log.Println("RemoteRig hub starting...") // Load config cfg, err := loadConfig("config.yaml") if err != nil { log.Fatalf("Failed to load config: %v", err) } // Open database sqlDB, err := db.Open(cfg.DBPath) if err != nil { log.Fatalf("Failed to open database: %v", err) } defer sqlDB.Close() log.Printf("Database open: %s", cfg.DBPath) // Create SSE hub for real-time updates sseHub := events.NewHub() // Start MQTT subscriber for ESP32 camera status ingestion mqttSub := mqtt.NewSubscriber(cfg.MQTT.Broker, cfg.MQTT.ClientID, sqlDB, sseHub) if err := mqttSub.Connect(); err != nil { log.Printf("WARNING: MQTT subscriber failed to connect: %v (running without MQTT)", err) } defer mqttSub.Close() // Set up router r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(cfg.WriteTimeout)) // Health check (no auth) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) // API routes (auth required if API key is configured) r.Mount("/api/v1", auth.Middleware(cfg.APIKey)(apiRouter(sseHub, sqlDB))) // Create server httpServer := &http.Server{ Addr: ":" + cfg.Port, Handler: r, ReadTimeout: cfg.ReadTimeout, WriteTimeout: cfg.WriteTimeout, IdleTimeout: cfg.IdleTimeout, } // Graceful shutdown go func() { sigInt := make(chan os.Signal, 1) signal.Notify(sigInt, syscall.SIGINT, syscall.SIGTERM) <-sigInt log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() httpServer.Shutdown(ctx) }() log.Printf("Server listening on port %s", cfg.Port) if err := httpServer.ListenAndServe(); err != nil { log.Fatalf("Server failed: %v", err) } } // apiRouter creates the API route tree. func apiRouter(sseHub *events.Hub, database *db.DB) http.Handler { r := chi.NewRouter() // Camera management routes r.Get("/cameras", api.ListCameras(database)) r.Post("/cameras", api.RegisterCamera(database)) r.Get("/cameras/{id}", api.GetCameraDetail(database)) // Recording control routes r.Post("/cameras/{id}/start", api.StartRecording(database)) r.Post("/cameras/{id}/stop", api.StopRecording(database)) // Status ingestion (from ESP32 nodes) r.Post("/cameras/{id}/status", api.PushStatus(database)) // Real-time events (SSE) r.Handle("/events/stream", sseHub.Handler()) return r } func loadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config file: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } // Defaults if cfg.Port == "" { cfg.Port = "8080" } if cfg.DBPath == "" { cfg.DBPath = "remoterig.db" } if cfg.MQTT.Broker == "" { cfg.MQTT.Broker = "localhost:1883" } if cfg.MQTT.ClientID == "" { cfg.MQTT.ClientID = "remoterig-hub" } return &cfg, nil }