diff --git a/backend/internal/api/api_test.go b/backend/internal/api/api_test.go index a4ded69..ed01148 100644 --- a/backend/internal/api/api_test.go +++ b/backend/internal/api/api_test.go @@ -664,6 +664,48 @@ func TestSyncReset_DeletesActivitiesAndBackfillEndpointIsGone(t *testing.T) { } } +func TestSyncStatus_ReportsPhaseAwareProgressAndWorkoutsPending(t *testing.T) { + s, db, userID := newTestServer(t) + ctx := newCtx() + + workoutID := int64(555) + if _, err := db.UpsertActivity(ctx, userID, store.Activity{ + GarminActivityID: 1, WorkoutID: &workoutID, + StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}", + }); err != nil { + t.Fatalf("UpsertActivity: %v", err) + } + + rec := doJSON(t, s.Router(), http.MethodGet, "/api/sync/status", nil) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String()) + } + var resp struct { + InProgress bool `json:"in_progress"` + Progress struct { + Phase string `json:"Phase"` + Done int `json:"Done"` + Total int `json:"Total"` + } `json:"progress"` + ActivitiesPendingDetails int `json:"activities_pending_details"` + WorkoutsPending int `json:"workouts_pending"` + } + unmarshalBody(t, rec, &resp) + + if resp.InProgress { + t.Error("in_progress = true, want false (nothing running)") + } + if resp.Progress.Phase != "idle" { + t.Errorf("progress.Phase = %q, want %q", resp.Progress.Phase, "idle") + } + if resp.ActivitiesPendingDetails != 1 { + t.Errorf("activities_pending_details = %d, want 1", resp.ActivitiesPendingDetails) + } + if resp.WorkoutsPending != 1 { + t.Errorf("workouts_pending = %d, want 1", resp.WorkoutsPending) + } +} + func TestProgression_ReturnsSortedTimeSeries(t *testing.T) { s, db, userID := newTestServer(t) ctx := newCtx() diff --git a/backend/internal/api/sync.go b/backend/internal/api/sync.go index 6a9e5f5..de86764 100644 --- a/backend/internal/api/sync.go +++ b/backend/internal/api/sync.go @@ -73,7 +73,12 @@ func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, err.Error()) return } - remaining, err := s.DB.CountActivitiesMissingDetails(r.Context(), userID) + pendingDetails, err := s.DB.CountActivitiesMissingDetails(r.Context(), userID) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + pendingWorkouts, err := s.DB.CountActivitiesMissingWorkout(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -92,8 +97,9 @@ func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) { resp := map[string]any{ "in_progress": inProgress, - "detail_fill_progress": progress, - "activities_pending_details": remaining, + "progress": progress, + "activities_pending_details": pendingDetails, + "workouts_pending": pendingWorkouts, } if ok { resp["last_run"] = run diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index fb7f7e2..088c2d2 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -173,7 +173,7 @@ export interface ProgressionPoint { export interface SyncRun { ID: number; - Kind: "backfill" | "incremental"; + Kind: "backfill" | "incremental" | "full"; StartedAt: string; FinishedAt: string | null; ActivitiesFetched: number; @@ -194,15 +194,17 @@ export interface SessionInfo { garmin_connected: boolean; } -export interface DetailFillProgress { +export interface SyncProgress { + Phase: "idle" | "discovering" | "activities" | "workouts"; Done: number; Total: number; } export interface SyncStatus { in_progress: boolean; - detail_fill_progress: DetailFillProgress; + progress: SyncProgress; activities_pending_details: number; + workouts_pending: number; last_run?: SyncRun; }