package api import ( "context" "net/http" ) // detailFillBatchSize bounds how many activities' details/splits are fetched // per sync trigger, matching the sequential rate-limited fetch in // internal/sync.Service.FillPendingDetails. const detailFillBatchSize = 50 // handleSyncRun does a full sync pass: Backfill first (resumes from the // watermark, so widening the configured history horizon between clicks is // picked up automatically), then IncrementalSync (catches anything new since // the latest known activity), then fills in details for whatever's still // missing them. Activities already fully processed are left untouched -- // see internal/sync.Service.FillPendingDetails. Recorded as a single // FullSync run so "last sync" reports the combined activity count, not just // whichever of Backfill/IncrementalSync happened to finish last. func (s *Server) handleSyncRun(w http.ResponseWriter, r *http.Request) { ok := s.backgroundSync(func(ctx context.Context) error { return s.Sync.FullSync(ctx, detailFillBatchSize) }) if !ok { writeError(w, http.StatusConflict, "a sync is already in progress") return } writeJSON(w, http.StatusAccepted, map[string]string{"status": "started"}) } // handleSyncReset wipes every synced activity (and its laps/samples/kind // assignments) and rewinds the backfill watermark, so the next Sync Now // performs a genuinely fresh pull from Garmin. Destructive -- the frontend // gates this behind a confirmation. func (s *Server) handleSyncReset(w http.ResponseWriter, r *http.Request) { ok := s.backgroundSync(func(ctx context.Context) error { return s.Sync.ResetAll(ctx) }) if !ok { writeError(w, http.StatusConflict, "a sync is already in progress") return } writeJSON(w, http.StatusAccepted, map[string]string{"status": "started"}) } func (s *Server) handleSyncRuns(w http.ResponseWriter, r *http.Request) { runs, err := s.DB.ListSyncRuns(r.Context(), 20) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, runs) } func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) { run, ok, err := s.DB.LatestSyncRun(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } remaining, err := s.DB.CountActivitiesMissingDetails(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.mu.Lock() inProgress := s.syncRunning s.mu.Unlock() progress := s.Sync.Progress() resp := map[string]any{ "in_progress": inProgress, "detail_fill_progress": progress, "activities_pending_details": remaining, } if ok { resp["last_run"] = run } writeJSON(w, http.StatusOK, resp) }