package main import ( "context" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/joho/godotenv" "git.seaofstars.xyz/mohd/deflated/internal/db" "git.seaofstars.xyz/mohd/deflated/internal/handlers" ) func main() { // Load .env file if present (ignored in production) _ = godotenv.Load() // Structured logger — prints human-readable text locally, JSON in prod logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, })) slog.SetDefault(logger) // Connect to Postgres pool, err := db.Connect(os.Getenv("DATABASE_URL")) if err != nil { slog.Error("failed to connect to database", "error", err) os.Exit(1) } defer pool.Close() // Run migrations on startup if err := db.Migrate(pool); err != nil { slog.Error("failed to run migrations", "error", err) os.Exit(1) } // Wire up dependencies queries := db.New(pool) receiptHandler := handlers.NewReceiptHandler(queries) itemHandler := handlers.NewItemHandler(queries) // Build router r := chi.NewRouter() // Middleware stack (runs for every request, in order) r.Use(middleware.RequestID) // adds X-Request-ID header r.Use(middleware.RealIP) // reads X-Forwarded-For for real client IP r.Use(middleware.Logger) // logs method, path, status, duration r.Use(middleware.Recoverer) // catches panics, returns 500 instead of crashing r.Use(middleware.Compress(5)) // gzip responses r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"http://localhost:5173", os.Getenv("FRONTEND_URL")}, AllowedMethods: []string{"GET", "POST", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Content-Type"}, })) // Routes r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") }) r.Route("/api", func(r chi.Router) { r.Post("/receipts", receiptHandler.Submit) r.Get("/receipts/{id}", receiptHandler.Get) r.Get("/items", itemHandler.List) r.Get("/items/{name}/history", itemHandler.PriceHistory) r.Get("/items/top-movers", itemHandler.TopMovers) r.Get("/inflation/summary", itemHandler.InflationSummary) }) // Server with graceful shutdown port := os.Getenv("PORT") if port == "" { port = "8080" } srv := &http.Server{ Addr: ":" + port, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } // Start server in background goroutine go func() { slog.Info("server starting", "port", port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server error", "error", err) os.Exit(1) } }() // Block until SIGINT or SIGTERM (Ctrl+C or `docker stop`) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("shutting down gracefully...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { slog.Error("forced shutdown", "error", err) } }