feat(sync): split FillPendingDetails into activities/workouts phases

Progress becomes phase-aware ({Phase, Done, Total} instead of a flat
{Done, Total}), with FullSync reporting an indeterminate "discovering"
phase during backfill/incremental sync (there's no meaningful total
before those calls have already happened), then FillPendingDetails
reporting a real Done/Total for its activities pass, then its new,
independent workouts pass.

alignWorkoutTargets now takes a lap count instead of a []garmin.Lap
slice, since it never used lap content, only length -- this lets the
new workouts pass call it against laps read back from the DB rather
than needing the original Garmin lap data again.

Splitting the passes also fixes a latent bug: an activity whose
details were fetched successfully but whose workout fetch failed in
that same run previously had no way to ever retry the workout fetch,
since ActivitiesMissingDetails stops returning it once
details_fetched_at/splits_fetched_at are set. ActivitiesMissingWorkout
queries workout_id/workout_raw_json independently, so it keeps
surfacing that activity until its workout is actually fetched.
This commit is contained in:
2026-07-27 06:55:23 +02:00
parent 32fdfc1c72
commit 35d9933d1b
4 changed files with 293 additions and 52 deletions

View File

@@ -10,11 +10,17 @@ import (
// Client is a fake garmin.Client returning data supplied by the test/caller.
type Client struct {
AuthResults []garmin.AuthResult // consumed in order by Authenticate/CompleteMFA calls
Activities []garmin.Activity
Splits map[int64]garmin.ActivitySplits
Details map[int64]garmin.ActivityDetails
Workouts map[int64]garmin.Workout
AuthResults []garmin.AuthResult // consumed in order by Authenticate/CompleteMFA calls
Activities []garmin.Activity
Splits map[int64]garmin.ActivitySplits
Details map[int64]garmin.ActivityDetails
Workouts map[int64]garmin.Workout
// WorkoutErrByID, if set for a given workout ID, makes GetWorkoutByID
// return that error for that ID specifically -- independent of the
// all-calls-fail Err field below -- so a test can simulate one
// activity's workout fetch failing while others in the same batch
// succeed.
WorkoutErrByID map[int64]error
Err error // if set, every call returns this error
authResultCursor int
ClosedCalled bool
@@ -79,6 +85,9 @@ func (c *Client) GetWorkoutByID(ctx context.Context, workoutID int64) (garmin.Wo
if c.Err != nil {
return garmin.Workout{}, c.Err
}
if err, ok := c.WorkoutErrByID[workoutID]; ok {
return garmin.Workout{}, err
}
return c.Workouts[workoutID], nil
}