Files
geniusrun/backend/cmd/smartrund/main.go
Christophe Vila 249826afc0 Move backfill horizon from a startup env var into the editable profile
SMARTRUN_BACKFILL_HORIZON_DAYS was a server-startup-only env var with no UI,
defaulting to 3 years -- so editing the unrelated "Rolling window" profile
field (for classification, not sync) had no effect on how far back Sync Now
reached. Backfill horizon is now Profile.BackfillHorizonDays, read fresh on
every Backfill call, with its own field on the Profile page.
2026-07-19 12:57:18 +02:00

95 lines
2.4 KiB
Go

// 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)
}
}
}
}