// Command geniusrund is geniusrun's backend server: syncs runs from Garmin, // classifies them into workout kinds, and serves the REST API the frontend // talks to. package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "geniusrun/backend/internal/api" "geniusrun/backend/internal/auth" "geniusrun/backend/internal/config" "geniusrun/backend/internal/garmin" applog "geniusrun/backend/internal/log" "geniusrun/backend/internal/store" ) // fatal logs through the application JSON logger and exits -- the // structured replacement for log.Fatalf, so even startup failures come // out as JSON lines. func fatal(msg string, err error) { applog.App("main", "fatal").Error(msg, "error", err) os.Exit(1) } func main() { // Bootstrap logger at info so failures loading the env config itself // (which carries the real log level) are still emitted as JSON. slog.SetDefault(applog.NewLogger("warn", os.Stdout)) envCfg, err := config.LoadEnv() if err != nil { fatal("load env config", err) } slog.SetDefault(applog.NewLogger(envCfg.LogLevel, os.Stdout)) db, err := store.Open(envCfg.DBPath) if err != nil { fatal("open database", err) } defer db.Close() values, err := db.ConfigValues(context.Background()) if err != nil { fatal("read app config", err) } // Every registry key is mandatory in the DB: seed missing ones with // their defaults so the config table is always fully populated (no // code-side fallbacks anywhere downstream). for _, k := range config.AppRegistry() { if _, ok := values[k.Key]; !ok { if err := db.SetConfigValue(context.Background(), k.Key, k.Default); err != nil { fatal("seed app config default", err) } values[k.Key] = k.Default } } appCfg, err := config.LoadApp(values) if err != nil { fatal("load app config", err) } authVerifier, err := auth.NewOIDCVerifier(context.Background(), auth.OIDCConfig{ IssuerURL: envCfg.OIDCIssuerURL, ClientID: envCfg.OIDCClientID, ClientSecret: envCfg.OIDCClientSecret, RedirectURL: envCfg.OIDCRedirectURL, RequiredRole: envCfg.OIDCRequiredRole, }) if err != nil { fatal("oidc verifier setup", err) } server := api.NewServer( db, garmin.NewClient, garmin.ClientConfig{ PythonPath: envCfg.PythonPath, TokenStorePath: envCfg.TokenStoreRoot, }, garmin.SyncConfig{}, authVerifier, api.SessionConfig{ Secret: envCfg.SessionSecret, Duration: appCfg.SessionDuration, SetupTimeout: appCfg.SetupTimeout, Secure: envCfg.SessionSecure, BackendURL: envCfg.BackendURL, FrontendURL: envCfg.FrontendURL, }) for _, e := range envCfg.DisplayEnv() { server.EnvVars = append(server.EnvVars, api.EnvVar{Name: e.Name, Value: e.Value}) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() httpServer := &http.Server{Addr: envCfg.BackendAddr, Handler: server.Router()} go func() { applog.App("main", "main").Info("geniusrund listening", "addr", envCfg.BackendAddr) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { fatal("http server", err) } }() <-ctx.Done() applog.App("main", "main").Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := httpServer.Shutdown(shutdownCtx); err != nil { applog.App("main", "main").Error("http server shutdown", "error", err) } }