// Command server runs the PilotVault API Server. It is the single entry point // the Fly App (drone/telemetry uplink) and the Web App / API Web Panel talk to. // It keeps live device state in memory, fans telemetry out to dashboards over a // websocket, and proxies authentication to a PocketBase kept behind it. package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "pilotvault/apiserver/internal/api" "pilotvault/apiserver/internal/config" "pilotvault/apiserver/internal/hub" ) func main() { log.SetFlags(log.LstdFlags | log.Lmsgprefix) log.SetPrefix("[api] ") cfg := config.Load() h := hub.New() srv := api.New(cfg, h) if err := srv.StartPlugins(); err != nil { log.Printf("plugins: load failed: %v", err) } httpServer := &http.Server{ Addr: cfg.Addr, Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second, // No WriteTimeout: /ws/* are long-lived streaming connections. IdleTimeout: 60 * time.Second, } go func() { log.Printf("listening on %s (PocketBase: %s, panel at /)", cfg.Addr, cfg.PocketBaseURL) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }() // Graceful shutdown. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop log.Println("shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() srv.Stop(ctx) if err := httpServer.Shutdown(ctx); err != nil { log.Printf("shutdown error: %v", err) } log.Println("stopped") }