// 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" "net/http" "os/signal" "syscall" "time" "geniusrun/backend/internal/api" "geniusrun/backend/internal/auth" "geniusrun/backend/internal/config" "geniusrun/backend/internal/garmin" "geniusrun/backend/internal/store" appsync "geniusrun/backend/internal/sync" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("config: %v", err) } db, err := store.Open(cfg.DBPath) if err != nil { log.Fatalf("open database: %v", err) } defer db.Close() authVerifier, err := auth.NewOIDCVerifier(context.Background(), auth.OIDCConfig{ IssuerURL: cfg.OIDCIssuerURL, ClientID: cfg.OIDCClientID, ClientSecret: cfg.OIDCClientSecret, RedirectURL: cfg.OIDCRedirectURL, RequiredRole: cfg.OIDCRequiredRole, }) if err != nil { log.Fatalf("oidc: %v", err) } server := api.NewServer(db, garmin.NewClient, garmin.Config{ PythonPath: cfg.GarminPythonPath, TokenStorePath: cfg.GarminTokenStoreRoot, }, appsync.Config{ MinConfidence: cfg.MinConfidence, }, authVerifier, api.SessionConfig{ Secret: cfg.SessionSecret, Duration: cfg.SessionDuration, Secure: cfg.SessionSecure, BackendURL: cfg.BackendURL, FrontendURL: cfg.FrontendURL, }) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go runIncrementalSyncLoop(ctx, server, cfg.IncrementalSyncEvery) httpServer := &http.Server{Addr: cfg.Addr, Handler: server.Router()} go func() { log.Printf("geniusrund listening on %s", cfg.Addr) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("http server: %v", err) } }() <-ctx.Done() log.Println("shutting down...") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := httpServer.Shutdown(shutdownCtx); err != nil { log.Printf("http server shutdown: %v", err) } } // runIncrementalSyncLoop periodically syncs new activities for every // provisioned user in the background so the frontend doesn't need to // trigger every sync manually. func runIncrementalSyncLoop(ctx context.Context, server *api.Server, every time.Duration) { ticker := time.NewTicker(every) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: server.RunIncrementalSyncForAllUsers(ctx) } } }