// Command smartrund is smartrun'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" "smartrun/backend/internal/api" "smartrun/backend/internal/config" "smartrun/backend/internal/garmin" "smartrun/backend/internal/store" appsync "smartrun/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() profile, err := db.GetProfile(context.Background()) if err != nil { log.Fatalf("load profile: %v", err) } garminClient := garmin.NewClient(garmin.Config{ PythonPath: cfg.GarminPythonPath, ServerPath: cfg.GarminServerPath, GarminEmail: profile.GarminEmail, GarminPassword: profile.GarminPassword, TokenStorePath: cfg.GarminTokenStore, }) defer garminClient.Close() syncSvc := appsync.NewService(garminClient, db, appsync.Config{ MinConfidence: cfg.MinConfidence, }, nil) server := api.NewServer(db, garminClient, syncSvc) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go runIncrementalSyncLoop(ctx, syncSvc, cfg.IncrementalSyncEvery) httpServer := &http.Server{Addr: cfg.Addr, Handler: server.Router()} go func() { log.Printf("smartrund 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 in the // background so the frontend doesn't need to trigger every sync manually. func runIncrementalSyncLoop(ctx context.Context, svc *appsync.Service, every time.Duration) { ticker := time.NewTicker(every) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if err := svc.IncrementalSync(ctx); err != nil { log.Printf("incremental sync: %v", err) continue } if err := svc.FillPendingDetails(ctx, 50); err != nil { log.Printf("fill pending details: %v", err) } } } }