package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "hopweb/internal/config" "hopweb/internal/gitea" "hopweb/internal/httpapi" "hopweb/internal/store" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) cfg, err := config.Load() if err != nil { logger.Error("invalid configuration", "error", err) os.Exit(1) } ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() db, err := store.Open(ctx, cfg.DatabaseURL) if err != nil { logger.Error("open database", "error", err) os.Exit(1) } defer db.Close() if err := db.Migrate(ctx); err != nil { logger.Error("migrate database", "error", err) os.Exit(1) } giteaClient, err := gitea.NewClient(cfg.GiteaBaseURL, cfg.GiteaAPIToken) if err != nil { logger.Error("configure gitea client", "error", err) os.Exit(1) } server := &http.Server{ Addr: cfg.HTTPAddr, Handler: httpapi.New(db, giteaClient, cfg.AdminToken, cfg.GiteaWebhookSecret, logger), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } errCh := make(chan error, 1) go func() { logger.Info("control plane listening", "addr", cfg.HTTPAddr) errCh <- server.ListenAndServe() }() select { case err := <-errCh: if !errors.Is(err, http.ErrServerClosed) { logger.Error("serve", "error", err) os.Exit(1) } case <-ctx.Done(): shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { logger.Error("shutdown", "error", err) os.Exit(1) } } }