feat(api): reshape /api/sync/status for phase-aware progress
Replaces detail_fill_progress ({Done, Total}) with progress ({Phase,
Done, Total}), and adds workouts_pending (mirroring
activities_pending_details) from the new CountActivitiesMissingWorkout.
Frontend types updated to match -- GarminConnection.tsx is intentionally
left broken by this commit alone; it's fixed in the next commit that
adds SyncModal.
This commit is contained in:
@@ -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) {
|
func TestProgression_ReturnsSortedTimeSeries(t *testing.T) {
|
||||||
s, db, userID := newTestServer(t)
|
s, db, userID := newTestServer(t)
|
||||||
ctx := newCtx()
|
ctx := newCtx()
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
@@ -92,8 +97,9 @@ func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
resp := map[string]any{
|
resp := map[string]any{
|
||||||
"in_progress": inProgress,
|
"in_progress": inProgress,
|
||||||
"detail_fill_progress": progress,
|
"progress": progress,
|
||||||
"activities_pending_details": remaining,
|
"activities_pending_details": pendingDetails,
|
||||||
|
"workouts_pending": pendingWorkouts,
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
resp["last_run"] = run
|
resp["last_run"] = run
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export interface ProgressionPoint {
|
|||||||
|
|
||||||
export interface SyncRun {
|
export interface SyncRun {
|
||||||
ID: number;
|
ID: number;
|
||||||
Kind: "backfill" | "incremental";
|
Kind: "backfill" | "incremental" | "full";
|
||||||
StartedAt: string;
|
StartedAt: string;
|
||||||
FinishedAt: string | null;
|
FinishedAt: string | null;
|
||||||
ActivitiesFetched: number;
|
ActivitiesFetched: number;
|
||||||
@@ -194,15 +194,17 @@ export interface SessionInfo {
|
|||||||
garmin_connected: boolean;
|
garmin_connected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DetailFillProgress {
|
export interface SyncProgress {
|
||||||
|
Phase: "idle" | "discovering" | "activities" | "workouts";
|
||||||
Done: number;
|
Done: number;
|
||||||
Total: number;
|
Total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncStatus {
|
export interface SyncStatus {
|
||||||
in_progress: boolean;
|
in_progress: boolean;
|
||||||
detail_fill_progress: DetailFillProgress;
|
progress: SyncProgress;
|
||||||
activities_pending_details: number;
|
activities_pending_details: number;
|
||||||
|
workouts_pending: number;
|
||||||
last_run?: SyncRun;
|
last_run?: SyncRun;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user