Add configurable pace-artifact filtering to replace the hardcoded cutoff

Two new Profile settings -- "minimum representative pace" and "minimum
representative time" -- replace the previous hardcoded 20:00/km cutoff. A
stretch of consecutive samples slower than the configured pace is now
dropped from the chart (and its Y-axis scale) only if it lasts no longer
than the configured time; a longer stretch is kept as a real stop or walk
break rather than noise. Defaults to 12:00/km and 3 seconds.

Caught a boundary bug while verifying against real data: a run lasting
exactly the threshold duration survived filtering because the comparison
used strict "<" instead of "<=", contradicting "not lasting more than N
seconds" (which should include exactly N).
This commit is contained in:
2026-07-19 17:38:50 +02:00
parent 619277b0ee
commit 513418123f
7 changed files with 170 additions and 14 deletions

View File

@@ -25,6 +25,9 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) {
if p.BackfillHorizonDays != 1095 {
t.Errorf("BackfillHorizonDays = %d, want 1095 (migration default)", p.BackfillHorizonDays)
}
if p.MinRepresentativePaceSecPerKm != 720 || p.MinRepresentativeTimeSeconds != 3 {
t.Errorf("pace artifact filter defaults = %+v, want pace=720, time=3", p)
}
maxHR, restingHR := 190.0, 50.0
p.GarminEmail = "runner@example.com"
@@ -34,6 +37,8 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) {
p.RestingHeartRate = &restingHR
p.WarmupMinutes = 8
p.BackfillHorizonDays = 14
p.MinRepresentativePaceSecPerKm = 600
p.MinRepresentativeTimeSeconds = 5
if err := db.UpdateProfile(ctx, p); err != nil {
t.Fatalf("UpdateProfile: %v", err)
@@ -55,4 +60,7 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) {
if got.BackfillHorizonDays != 14 {
t.Errorf("BackfillHorizonDays = %v, want 14", got.BackfillHorizonDays)
}
if got.MinRepresentativePaceSecPerKm != 600 || got.MinRepresentativeTimeSeconds != 5 {
t.Errorf("pace artifact filter after update = %+v, want pace=600, time=5", got)
}
}