diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 06:56:12 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 06:56:12 +0200 |
| commit | 9da52d7355f5a77f0adc9a658619b3bfef03ba3c (patch) | |
| tree | b9e5f803d0c9787a84696b9e38b06b6721c9ded3 /web | |
| parent | 13aeedfbb9271444ca42efb45bbe0986ae7610ce (diff) | |
feat(ride): persist canonical moving time, preserve Strava reference
Diffstat (limited to 'web')
| -rw-r--r-- | web/ride-filters.go | 4 | ||||
| -rw-r--r-- | web/ride-filters_test.go | 10 | ||||
| -rw-r--r-- | web/server.go | 4 | ||||
| -rw-r--r-- | web/server_test.go | 21 | ||||
| -rw-r--r-- | web/templates.templ | 4 | ||||
| -rw-r--r-- | web/templates_templ.go | 14 | ||||
| -rw-r--r-- | web/views.go | 1 |
7 files changed, 39 insertions, 19 deletions
diff --git a/web/ride-filters.go b/web/ride-filters.go index 189a72e..3b567f5 100644 --- a/web/ride-filters.go +++ b/web/ride-filters.go @@ -271,10 +271,10 @@ func rideMatchesFilters(item rides.Ride, filters RideFilters) bool { if filters.DistanceMaxKm != nil && item.DistanceM > float64(*filters.DistanceMaxKm)*1000 { return false } - if filters.DurationMinMinutes != nil && item.MovingTimeS < int64(*filters.DurationMinMinutes)*60 { + if filters.DurationMinMinutes != nil && item.EffectiveMovingTimeS() < int64(*filters.DurationMinMinutes)*60 { return false } - if filters.DurationMaxMinutes != nil && item.MovingTimeS > int64(*filters.DurationMaxMinutes)*60 { + if filters.DurationMaxMinutes != nil && item.EffectiveMovingTimeS() > int64(*filters.DurationMaxMinutes)*60 { return false } startDate := item.StartDate.UTC() diff --git a/web/ride-filters_test.go b/web/ride-filters_test.go index c7b9133..8b470b6 100644 --- a/web/ride-filters_test.go +++ b/web/ride-filters_test.go @@ -89,11 +89,11 @@ func TestParseRideFiltersTreatsFullRangesAsInactive(t *testing.T) { func TestFilterRidesUsesInclusiveBoundsAndMovingTime(t *testing.T) { items := []rides.Ride{ - {Name: "lower boundary", DistanceM: 20_000, MovingTimeS: 1_800, StartDate: time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC), GPXPath: "lower.gpx"}, - {Name: "upper boundary", DistanceM: 100_000, MovingTimeS: 10_800, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "upper.gpx"}, - {Name: "outside distance", DistanceM: 110_000, MovingTimeS: 10_800, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "outside-distance.gpx"}, - {Name: "outside moving time", DistanceM: 100_000, MovingTimeS: 10_801, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "outside-duration.gpx"}, - {Name: "outside date", DistanceM: 100_000, MovingTimeS: 10_800, StartDate: time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC), GPXPath: "outside-date.gpx"}, + {Name: "lower boundary", DistanceM: 20_000, ExternalMovingTimeS: 1_800, StartDate: time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC), GPXPath: "lower.gpx"}, + {Name: "upper boundary", DistanceM: 100_000, ExternalMovingTimeS: 10_800, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "upper.gpx"}, + {Name: "outside distance", DistanceM: 110_000, ExternalMovingTimeS: 10_800, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "outside-distance.gpx"}, + {Name: "outside moving time", DistanceM: 100_000, ExternalMovingTimeS: 10_801, StartDate: time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC), GPXPath: "outside-duration.gpx"}, + {Name: "outside date", DistanceM: 100_000, ExternalMovingTimeS: 10_800, StartDate: time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC), GPXPath: "outside-date.gpx"}, } filters := parseRideFilters(url.Values{ rideFilterDistanceMinParam: {"20"}, diff --git a/web/server.go b/web/server.go index 4bc74e3..06ee425 100644 --- a/web/server.go +++ b/web/server.go @@ -228,7 +228,7 @@ func buildRideViews(items []rides.Ride) []RideView { } func buildRideView(item rides.Ride) RideView { - view := RideView{Ride: item, Cotacol: "-", CotacolPer100Km: "-"} + view := RideView{Ride: item, Cotacol: "-", CotacolPer100Km: "-", MovingTime: formatDuration(item.EffectiveMovingTimeS())} cotacol, ready := item.CotacolScore() if ready && item.CotacolAlgorithmVersion() == ride.CotacolAlgorithmVersion && item.DistanceM > 0 { view.Cotacol = formatCotacol(cotacol) @@ -459,7 +459,7 @@ func rideFromActivity(externalID, gpxPath string, activity strava.Activity) ride Type: activityType, StartDate: activity.StartDate, DistanceM: activity.DistanceM, - MovingTimeS: activity.MovingTimeS, + ExternalMovingTimeS: activity.MovingTimeS, ElapsedTimeS: activity.ElapsedTimeS, TotalElevationGainM: activity.TotalElevationGainM, AverageSpeedMps: activity.AverageSpeedMps, diff --git a/web/server_test.go b/web/server_test.go index 823e4a8..68928fe 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -361,7 +361,7 @@ func TestHandlerReturnsNotFoundForUnknownRide(t *testing.T) { func TestHandlerShowsErrorForUnavailableRideRoute(t *testing.T) { server, db := newWebTestServer(t) _, err := db.Exec(` - INSERT INTO rides (external_id, gpx_path, name, type, start_date, distance_m, moving_time_s, elapsed_time_s, total_elevation_gain_m, average_speed_mps) + INSERT INTO rides (external_id, gpx_path, name, type, start_date, distance_m, external_moving_time_s, elapsed_time_s, total_elevation_gain_m, average_speed_mps) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8", "missing.gpx", "Unavailable Route", "Ride", "2026-08-01T07:00:00Z", 20_000, 0, 0, 0, 0) require.NoError(t, err) @@ -648,6 +648,25 @@ func TestBuildRideViewsUsesStoredCotacol(t *testing.T) { assert.NotEqual(t, "-", views[0].Cotacol) } +func TestBuildRideViewMovingTimePrefersCanonical(t *testing.T) { + _, db := newWebTestServer(t) + ride := rides.Ride{ + ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000", + GPXPath: testGPXPath(t, "moving.gpx"), + Name: "Moving Ride", + Type: "Ride", + StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC), + DistanceM: 20_000, + ExternalMovingTimeS: 7_200, + } + require.NoError(t, rides.Save(db, ride)) + items, err := rides.List(db) + require.NoError(t, err) + require.Len(t, items, 1) + + assert.NotEqual(t, "2h 00m", buildRideView(items[0]).MovingTime) +} + func TestHandlerSortsRidesByDistance(t *testing.T) { server, db := newWebTestServer(t) require.NoError(t, rides.Save(db, rides.Ride{ diff --git a/web/templates.templ b/web/templates.templ index 8a987fc..88f17d1 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -108,7 +108,7 @@ templ RidesContent(data RidesPageData) { </tr></thead> <tbody> for _, item := range data.Items { - <tr><td><strong><a href={ templ.URL(rideDetailURL(item.ID)) }>{ item.Name }</a></strong><br/><small>{ item.Type }</small></td><td>{ formatRideDate(item.StartDate) }</td><td class="numeric">{ formatDistance(item.DistanceM) }</td><td class="numeric">{ formatDuration(item.MovingTimeS) }</td><td class="numeric">{ formatElevation(item.TotalElevationGainM) }</td><td class="numeric">{ item.Cotacol }</td><td class="numeric">{ item.CotacolPer100Km }</td></tr> + <tr><td><strong><a href={ templ.URL(rideDetailURL(item.ID)) }>{ item.Name }</a></strong><br/><small>{ item.Type }</small></td><td>{ formatRideDate(item.StartDate) }</td><td class="numeric">{ formatDistance(item.DistanceM) }</td><td class="numeric">{ item.MovingTime }</td><td class="numeric">{ formatElevation(item.TotalElevationGainM) }</td><td class="numeric">{ item.Cotacol }</td><td class="numeric">{ item.CotacolPer100Km }</td></tr> } </tbody> </table> @@ -139,7 +139,7 @@ templ RideDetailContent(data RideDetailView) { <section class="panel summary-panel"> <dl> <div><dt>Distance</dt><dd>{ formatDistance(data.Ride.DistanceM) }</dd></div> - <div><dt>Moving time</dt><dd>{ formatDuration(data.Ride.MovingTimeS) }</dd></div> + <div><dt>Moving time</dt><dd>{ data.MovingTime }</dd></div> <div><dt>Elevation</dt><dd>{ formatElevation(data.Ride.TotalElevationGainM) }</dd></div> </dl> </section> diff --git a/web/templates_templ.go b/web/templates_templ.go index 05ae20a..a1cf867 100644 --- a/web/templates_templ.go +++ b/web/templates_templ.go @@ -821,9 +821,9 @@ func RidesContent(data RidesPageData) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(formatDuration(item.MovingTimeS)) + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(item.MovingTime) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 288} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 271} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { @@ -836,7 +836,7 @@ func RidesContent(data RidesPageData) templ.Component { var templ_7745c5c3_Var57 string templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(formatElevation(item.TotalElevationGainM)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 358} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 341} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { @@ -849,7 +849,7 @@ func RidesContent(data RidesPageData) templ.Component { var templ_7745c5c3_Var58 string templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(item.Cotacol) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 399} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 382} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { @@ -862,7 +862,7 @@ func RidesContent(data RidesPageData) templ.Component { var templ_7745c5c3_Var59 string templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(item.CotacolPer100Km) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 448} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 111, Col: 431} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) if templ_7745c5c3_Err != nil { @@ -1054,9 +1054,9 @@ func RideDetailContent(data RideDetailView) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var69 string - templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(formatDuration(data.Ride.MovingTimeS)) + templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(data.MovingTime) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 142, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/templates.templ`, Line: 142, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) if templ_7745c5c3_Err != nil { diff --git a/web/views.go b/web/views.go index de4d38f..1bf1782 100644 --- a/web/views.go +++ b/web/views.go @@ -16,6 +16,7 @@ type RideView struct { rides.Ride Cotacol string CotacolPer100Km string + MovingTime string } type RidesPageData struct { |
