// Command admin-dashboard is a stateless, server-rendered web UI over // control-api's /api/v1/admin/* HTTP API — see docs/DASHBOARD.md. It never // connects to the database and holds no state of its own. package main import ( "context" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "cloudipvalidator/internal/config" "cloudipvalidator/internal/dashboard" ) func main() { configPath := flag.String("config", "configs/admin-dashboard.yaml", "path to admin-dashboard config file") flag.Parse() log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) if err := run(*configPath, log); err != nil { log.Error("fatal", "err", err) os.Exit(1) } } func run(configPath string, log *slog.Logger) error { cfg, err := config.LoadAdminDashboard(configPath) if err != nil { return fmt.Errorf("load config: %w", err) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() controlAPIToken := os.Getenv(cfg.ControlAPI.TokenEnv) if controlAPIToken == "" { log.Warn("control-api admin token is not set: calls to control-api are sent without credentials", "env", cfg.ControlAPI.TokenEnv) } srv, err := dashboard.New(dashboard.Config{ ControlAPIBaseURL: cfg.ControlAPI.BaseURL, ControlAPITimeout: time.Duration(cfg.ControlAPI.TimeoutSeconds) * time.Second, LastCompletedCount: cfg.Overview.LastCompletedCount, OverviewPollIntervalS: cfg.Overview.PollIntervalSeconds, ControlAPIToken: controlAPIToken, Username: os.Getenv(cfg.Auth.UsernameEnv), Password: os.Getenv(cfg.Auth.PasswordEnv), SessionSecret: os.Getenv(cfg.Auth.SessionSecretEnv), SessionTTL: time.Duration(cfg.Auth.SessionTTLMinutes) * time.Minute, }, log) if err != nil { return fmt.Errorf("init dashboard: %w", err) } httpServer := &http.Server{Addr: cfg.Server.ListenAddr, Handler: srv.Handler()} errCh := make(chan error, 1) go func() { log.Info("listening", "addr", cfg.Server.ListenAddr, "control_api", cfg.ControlAPI.BaseURL) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- err } }() select { case <-ctx.Done(): log.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return httpServer.Shutdown(shutdownCtx) case err := <-errCh: return err } }