Dedup Garmin data storage, configurable chart colors, taxonomy fixes, sync/progression fixes
- Remove duplicated Garmin fields from storage; decode display-only fields (activity name/type, lap duration/HR, structured workout raw JSON) from RawJSON at API-response time instead of storing redundant columns - Add a fully configurable chart color system (pace/HR main-line colors, 4 effort-kind colors, tint/darken/brighten intensity knobs) under Profile > Chart colors - Rename training types and fix their display order (Easy, Long, 60'/30' Threshold, Tempo, Intervals, MAS Test, Race) everywhere they're listed - Add an Efficiency Factor progression metric; fix Progression chart axes to use tight non-zero-based domains, m:ss/km pace formatting, and rounded ticks instead of raw floating-point labels - Expose the raw get_workout_by_id() payload in the raw-data viewer alongside activity/lap/detail JSON; enlarge the modal and shrink array indentation for readability - Fix "last sync" reporting a meaningless activity count: record one combined sync run per manual "Sync now" and count genuinely new activities instead of re-listing whatever Garmin returned for the queried window - Let a Review Queue activity be manually cleared back to Unclassified, and make "Reset all" available even while disconnected from Garmin Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -555,6 +555,66 @@ func TestBackfill_ResumesFromWatermarkWhenHorizonGrows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullSync_RecordsOneCombinedSyncRun(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
m := &mock.Client{
|
||||
Activities: []garmin.Activity{
|
||||
{ActivityID: 1, ActivityType: garmin.ActivityType{TypeKey: "running"}, StartTimeGMT: "2026-07-05 06:00:00", Distance: 5000, Duration: 1500},
|
||||
{ActivityID: 2, ActivityType: garmin.ActivityType{TypeKey: "running"}, StartTimeGMT: "2026-07-08 06:00:00", Distance: 5000, Duration: 1500},
|
||||
},
|
||||
Splits: map[int64]garmin.ActivitySplits{
|
||||
1: {ActivityID: 1}, 2: {ActivityID: 2},
|
||||
},
|
||||
Details: map[int64]garmin.ActivityDetails{
|
||||
1: {ActivityID: 1}, 2: {ActivityID: 2},
|
||||
},
|
||||
}
|
||||
svc := NewService(m, db, Config{BackfillWindowDays: 10},
|
||||
fixedNow(time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC)))
|
||||
setBackfillHorizon(t, db, 10)
|
||||
|
||||
if err := svc.FullSync(ctx, 10); err != nil {
|
||||
t.Fatalf("FullSync: %v", err)
|
||||
}
|
||||
|
||||
runs, err := db.ListSyncRuns(ctx, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSyncRuns: %v", err)
|
||||
}
|
||||
if len(runs) != 1 {
|
||||
t.Fatalf("expected exactly 1 sync run recorded by FullSync (not one per stage), got %d: %+v", len(runs), runs)
|
||||
}
|
||||
run := runs[0]
|
||||
if run.Kind != store.SyncKindFull {
|
||||
t.Errorf("Kind = %q, want %q", run.Kind, store.SyncKindFull)
|
||||
}
|
||||
if run.Status != store.SyncStatusSuccess {
|
||||
t.Errorf("Status = %q, want success", run.Status)
|
||||
}
|
||||
// Backfill (one window covering the whole horizon) stores both of the
|
||||
// mock's activities as genuinely new (2). IncrementalSync then runs its
|
||||
// own separate fetch and -- since the fake client ignores the date range
|
||||
// it's called with -- sees the exact same 2 activities again, but they're
|
||||
// already stored by then, so it contributes 0 new ones. The combined
|
||||
// run's count (2) must reflect that dedup, not naively sum each stage's
|
||||
// raw fetch count (which would double-count to 4) or report only
|
||||
// whichever stage happened to run last (which would silently drop
|
||||
// Backfill's count) -- both are bugs this test guards against.
|
||||
if run.ActivitiesFetched != 2 {
|
||||
t.Errorf("ActivitiesFetched = %d, want 2 (genuinely new activities, deduped across stages)", run.ActivitiesFetched)
|
||||
}
|
||||
|
||||
activities, err := db.ListActivities(ctx, store.ActivityFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListActivities: %v", err)
|
||||
}
|
||||
if len(activities) != 2 {
|
||||
t.Fatalf("expected 2 stored activities (upserted, not duplicated), got %d", len(activities))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillPendingDetails_ReportsLiveProgress(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user