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.
95 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|