// Command server is the central Car Control API Server. It is the single // gateway between clients (web app, phone app, Home Assistant, ESP32 device) // and the PocketBase database. package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "carcontrol/api/internal/api" "carcontrol/api/internal/config" "carcontrol/api/internal/pb" ) func main() { log.SetFlags(log.LstdFlags | log.Lmsgprefix) log.SetPrefix("[api] ") cfg, err := config.Load() if err != nil { log.Fatalf("config: %v", err) } client := pb.New(cfg.PBURL, cfg.PBAdminEmail, cfg.PBAdminPasswd) authCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := client.Authenticate(authCtx); err != nil { log.Fatalf("pocketbase auth (%s): %v", cfg.PBURL, err) } log.Printf("authenticated to PocketBase at %s", cfg.PBURL) if cfg.UsingDevAuthSecret() { log.Println("WARNING: AUTH_SECRET is unset — using an insecure development secret. Set AUTH_SECRET in production.") } srv := api.NewServer(client, api.Options{ CORSOrigins: cfg.CORSOrigins, AuthSecret: cfg.AuthSecret, UsersCollection: cfg.UsersCollection, }) httpSrv := &http.Server{ Addr: announcedAddr(cfg.Port), Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second, } go func() { log.Printf("listening on %s", httpSrv.Addr) if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("http server: %v", err) } }() // Graceful shutdown on SIGINT/SIGTERM. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop log.Println("shutting down...") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() if err := httpSrv.Shutdown(shutdownCtx); err != nil { log.Printf("shutdown: %v", err) } } func announcedAddr(port string) string { if port == "" { port = "8080" } return ":" + port }