package main import ( "context" "errors" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" // Embed the timezone database so eBay's Pacific-time quota reset resolves // correctly even on minimal hosts without system zoneinfo. _ "time/tzdata" "veola/internal/apify" "veola/internal/auth" "veola/internal/config" "veola/internal/crypto" "veola/internal/db" "veola/internal/handlers" "veola/internal/ntfy" "veola/internal/scheduler" ) func main() { configPath := flag.String("config", "config.toml", "path to config TOML file") debug := flag.Bool("debug", false, "enable debug-level logging (verbose; raw external payloads logged)") flag.Parse() level := slog.LevelInfo if *debug { level = slog.LevelDebug } slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))) if *debug { slog.Debug("debug logging enabled") } if err := run(*configPath); err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } func run(configPath string) error { cfg, err := config.Load(configPath) if err != nil { return fmt.Errorf("config: %w", err) } key, err := crypto.DeriveKey([]byte(cfg.Security.EncryptionKey)) if err != nil { return fmt.Errorf("derive key: %w", err) } sqlDB, err := db.Open(cfg.Server.DBPath) if err != nil { return fmt.Errorf("db open: %w", err) } defer sqlDB.Close() store := db.NewStore(sqlDB, key) authMgr, err := auth.NewManager(sqlDB, store, cfg.Security.SessionSecret, cfg.Server.UseSecureCookies()) if err != nil { return fmt.Errorf("auth manager: %w", err) } apifyClient := apify.New(cfg.Apify.APIKey) ntfyClient := ntfy.New(cfg.Ntfy.BaseURL) sched := scheduler.New(cfg, store, apifyClient, ntfyClient) startCtx, cancelStart := context.WithTimeout(context.Background(), 30*time.Second) defer cancelStart() if err := sched.Start(startCtx); err != nil { return fmt.Errorf("scheduler start: %w", err) } app := handlers.New(cfg, store, authMgr, apifyClient, ntfyClient, sched) addr := fmt.Sprintf(":%d", cfg.Server.Port) srv := &http.Server{ Addr: addr, Handler: app.Routes(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } errCh := make(chan error, 1) go func() { slog.Info("listening", "addr", addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } close(errCh) }() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) select { case sig := <-sigCh: slog.Info("shutting down", "signal", sig.String()) case err := <-errCh: if err != nil { return fmt.Errorf("http: %w", err) } } shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second) defer cancelShutdown() if err := srv.Shutdown(shutdownCtx); err != nil { slog.Error("http shutdown", "err", err) } sched.Stop() slog.Info("shutdown complete") return nil }