From 53615a05708ff852af42905e2b5ced66ba70c43d Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Sun, 23 Aug 2026 11:09:07 +0200 Subject: Merge ride and rides packages into ride/store following mountain_pass structure Consolidate persistence (Save, List, Backfill) into ride/store.go alongside ride.go/climb.go/moving_time.go, rename rides.Ride to ride.StoredRide to avoid collision with parsed ride.Ride, update all web/cli consumers and RideView to use named Ride field, adjust templates and regenerate templ files, and remove rides directory. --- web/ride-filters.go | 7 ++--- web/ride-filters_test.go | 23 ++++++++-------- web/rides.go | 17 ++++++------ web/server_test.go | 71 ++++++++++++++++++++++++------------------------ web/sync.go | 12 ++++---- web/templates.templ | 2 +- web/views.go | 27 +++++++++--------- 7 files changed, 77 insertions(+), 82 deletions(-) (limited to 'web') diff --git a/web/ride-filters.go b/web/ride-filters.go index 1fb647b..64bade9 100644 --- a/web/ride-filters.go +++ b/web/ride-filters.go @@ -6,7 +6,6 @@ import ( "time" "github.com/martinlehoux/biking_home/ride" - "github.com/martinlehoux/biking_home/rides" ) const ( @@ -321,11 +320,11 @@ func (filters RideFilters) Active() bool { filters.Route != rideRouteFilterAny } -func filterRides(items []rides.Ride, filters RideFilters) []rides.Ride { +func filterRides(items []ride.StoredRide, filters RideFilters) []ride.StoredRide { if !filters.Active() { return items } - filtered := make([]rides.Ride, 0, len(items)) + filtered := make([]ride.StoredRide, 0, len(items)) for _, item := range items { if rideMatchesFilters(item, filters) { filtered = append(filtered, item) @@ -334,7 +333,7 @@ func filterRides(items []rides.Ride, filters RideFilters) []rides.Ride { return filtered } -func rideMatchesFilters(item rides.Ride, filters RideFilters) bool { +func rideMatchesFilters(item ride.StoredRide, filters RideFilters) bool { if filters.DistanceMinKm != nil && item.DistanceM < float64(*filters.DistanceMinKm)*1000 { return false } diff --git a/web/ride-filters_test.go b/web/ride-filters_test.go index 8b470b6..bf1cc6f 100644 --- a/web/ride-filters_test.go +++ b/web/ride-filters_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/martinlehoux/biking_home/ride" - "github.com/martinlehoux/biking_home/rides" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -88,7 +87,7 @@ func TestParseRideFiltersTreatsFullRangesAsInactive(t *testing.T) { } func TestFilterRidesUsesInclusiveBoundsAndMovingTime(t *testing.T) { - items := []rides.Ride{ + items := []ride.StoredRide{ {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"}, @@ -112,7 +111,7 @@ func TestFilterRidesUsesInclusiveBoundsAndMovingTime(t *testing.T) { } func TestFilterRidesByRouteAvailability(t *testing.T) { - items := []rides.Ride{ + items := []ride.StoredRide{ {Name: "recorded", GPXPath: "recorded.gpx"}, {Name: "metadata only"}, } @@ -128,7 +127,7 @@ func TestFilterRidesByRouteAvailability(t *testing.T) { func TestFilterRidesByCotacolExcludesUnavailableScores(t *testing.T) { _, db := newWebTestServer(t) - current := rides.Ride{ + current := ride.StoredRide{ ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000", GPXPath: testGPXPath(t, "current-cotacol.gpx"), Name: "Current score", @@ -136,7 +135,7 @@ func TestFilterRidesByCotacolExcludesUnavailableScores(t *testing.T) { StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC), DistanceM: 20_000, } - stale := rides.Ride{ + stale := ride.StoredRide{ ExternalID: "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8", GPXPath: testGPXPath(t, "stale-cotacol.gpx"), Name: "Stale score", @@ -144,16 +143,16 @@ func TestFilterRidesByCotacolExcludesUnavailableScores(t *testing.T) { StartDate: time.Date(2026, 8, 2, 7, 0, 0, 0, time.UTC), DistanceM: 20_000, } - unavailable := rides.Ride{ + unavailable := ride.StoredRide{ ExternalID: "strava:7ba7b810-9dad-41d1-80b4-00c04fd430c8", Name: "No score", Type: "VirtualRide", StartDate: time.Date(2026, 8, 3, 7, 0, 0, 0, time.UTC), DistanceM: 20_000, } - require.NoError(t, rides.Save(db, current)) - require.NoError(t, rides.Save(db, stale)) - require.NoError(t, rides.Save(db, unavailable)) + require.NoError(t, ride.Save(db, current)) + require.NoError(t, ride.Save(db, stale)) + require.NoError(t, ride.Save(db, unavailable)) _, err := db.Exec(` UPDATE rides SET cotacol_score = CASE external_id @@ -167,7 +166,7 @@ func TestFilterRidesByCotacolExcludesUnavailableScores(t *testing.T) { WHERE external_id IN (?, ?) `, current.ExternalID, stale.ExternalID, current.ExternalID, ride.CotacolAlgorithmVersion, stale.ExternalID, current.ExternalID, stale.ExternalID) require.NoError(t, err) - items, err := rides.List(db) + items, err := ride.List(db) require.NoError(t, err) filters := parseRideFilters(url.Values{rideFilterCotacolMinParam: {"200"}, rideFilterCotacolMaxParam: {"300"}}) @@ -210,7 +209,7 @@ func TestRideSortHeadersPreserveFilters(t *testing.T) { func TestHandlerFiltersRidesBeforeRenderingSortedResults(t *testing.T) { server, db := newWebTestServer(t) save := func(externalID, name, path string, distance float64) { - require.NoError(t, rides.Save(db, rides.Ride{ + require.NoError(t, ride.Save(db, ride.StoredRide{ ExternalID: externalID, GPXPath: testGPXPath(t, path), Name: name, @@ -235,7 +234,7 @@ func TestHandlerFiltersRidesBeforeRenderingSortedResults(t *testing.T) { func TestHandlerRendersFilteredEmptyState(t *testing.T) { server, db := newWebTestServer(t) - require.NoError(t, rides.Save(db, rides.Ride{ + require.NoError(t, ride.Save(db, ride.StoredRide{ ExternalID: "strava:8ba7b810-9dad-41d1-80b4-00c04fd430c8", GPXPath: testGPXPath(t, "empty-filter.gpx"), Name: "Short Ride", diff --git a/web/rides.go b/web/rides.go index cb38b3c..477f066 100644 --- a/web/rides.go +++ b/web/rides.go @@ -9,7 +9,6 @@ import ( "github.com/martinlehoux/biking_home/mountain_pass" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" - "github.com/martinlehoux/biking_home/rides" "github.com/martinlehoux/kagamigo/kcore" ) @@ -41,7 +40,7 @@ func (s *Server) handleRide(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } - item, found, err := rides.GetByID(s.db, id) + item, found, err := ride.GetByID(s.db, id) if err != nil { slog.Error("Failed to load ride", "ride_id", id, "error", err) http.Error(w, "failed to load ride", http.StatusInternalServerError) @@ -103,7 +102,7 @@ func (s *Server) handleAnalytics(w http.ResponseWriter, r *http.Request) { if filters.DistanceMaxKm != nil { maxKm = *filters.DistanceMaxKm } - eligible := make([]rides.Ride, 0, len(items)) + eligible := make([]ride.StoredRide, 0, len(items)) for _, item := range items { distanceKm := int(item.DistanceM / 1000) if distanceKm < minKm || (maxKm != 0 && distanceKm > maxKm) { @@ -130,7 +129,7 @@ func (s *Server) handleCreateOfficialClimb(w http.ResponseWriter, r *http.Reques http.NotFound(w, r) return } - item, found, err := rides.GetByID(s.db, id) + item, found, err := ride.GetByID(s.db, id) if err != nil { slog.Error("Failed to load ride for official climb creation", "ride_id", id, "error", err) http.Error(w, "failed to load ride", http.StatusInternalServerError) @@ -190,18 +189,18 @@ func parseRouteIndex(value string) (int, error) { return index, nil } -func (s *Server) listRides(rideSort RideSort) ([]rides.Ride, error) { +func (s *Server) listRides(rideSort RideSort) ([]ride.StoredRide, error) { column, found := rideSort.databaseColumn() if !found { - return rides.List(s.db) + return ride.List(s.db) } - return rides.ListSorted(s.db, column, rideSort.Descending) + return ride.ListSorted(s.db, column, rideSort.Descending) } const minimumDisplayedDistanceM = 10_000 const rideDetailMountainPassMarginM = 10_000 -func buildRideViews(items []rides.Ride) []RideView { +func buildRideViews(items []ride.StoredRide) []RideView { views := make([]RideView, 0, len(items)) for _, item := range items { if item.DistanceM < minimumDisplayedDistanceM { @@ -212,7 +211,7 @@ func buildRideViews(items []rides.Ride) []RideView { return views } -func buildRideView(item rides.Ride) RideView { +func buildRideView(item ride.StoredRide) RideView { view := RideView{Ride: item, Cotacol: "-", CotacolPer100Km: "-", MovingTime: formatDuration(item.EffectiveMovingTimeS())} cotacol, ready := item.CotacolScore() if ready && item.CotacolAlgorithmVersion() == ride.CotacolAlgorithmVersion && item.DistanceM > 0 { diff --git a/web/server_test.go b/web/server_test.go index 0514dfc..ebedcd6 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -20,7 +20,6 @@ import ( "github.com/martinlehoux/biking_home/mountain_pass" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" - "github.com/martinlehoux/biking_home/rides" "github.com/martinlehoux/biking_home/strava" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -73,7 +72,7 @@ const officialClimbTrackGPX = ` for _, item := range data.Items { - { item.Name }
{ item.Type }{ formatRideDate(item.StartDate) }{ formatDistance(item.DistanceM) }{ item.MovingTime }{ formatSpeed(item.AverageSpeedMps) }{ formatElevation(item.TotalElevationGainM) }{ item.Cotacol }{ item.CotacolPer100Km } + { item.Ride.Name }
{ item.Ride.Type }{ formatRideDate(item.Ride.StartDate) }{ formatDistance(item.Ride.DistanceM) }{ item.MovingTime }{ formatSpeed(item.Ride.AverageSpeedMps) }{ formatElevation(item.Ride.TotalElevationGainM) }{ item.Cotacol }{ item.CotacolPer100Km } } diff --git a/web/views.go b/web/views.go index 7a5a461..64afb82 100644 --- a/web/views.go +++ b/web/views.go @@ -9,11 +9,10 @@ import ( "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" "github.com/martinlehoux/biking_home/rideanalysis" - "github.com/martinlehoux/biking_home/rides" ) type RideView struct { - rides.Ride + Ride ride.StoredRide Cotacol string CotacolPer100Km string MovingTime string @@ -39,7 +38,7 @@ type AnalyticsPoint struct { CotacolPer100Km float64 `json:"cotacolPer100Km"` } -func buildAnalyticsPoints(items []rides.Ride) []AnalyticsPoint { +func buildAnalyticsPoints(items []ride.StoredRide) []AnalyticsPoint { points := make([]AnalyticsPoint, 0, len(items)) for _, item := range items { cotacol, ready := item.CotacolScore() @@ -194,16 +193,16 @@ func parseRideSort(query url.Values) RideSort { return RideSort{Column: column, Descending: direction != "asc"} } -func (s RideSort) databaseColumn() (rides.SortColumn, bool) { - columns := map[string]rides.SortColumn{ - rideSortName: rides.SortName, - rideSortStarted: rides.SortStartDate, - rideSortDistance: rides.SortDistance, - rideSortMovingTime: rides.SortMovingTime, - rideSortSpeed: rides.SortSpeed, - rideSortElevation: rides.SortElevation, - rideSortCotacol: rides.SortCotacol, - rideSortCotacolKm: rides.SortCotacolKm, +func (s RideSort) databaseColumn() (ride.SortColumn, bool) { + columns := map[string]ride.SortColumn{ + rideSortName: ride.SortName, + rideSortStarted: ride.SortStartDate, + rideSortDistance: ride.SortDistance, + rideSortMovingTime: ride.SortMovingTime, + rideSortSpeed: ride.SortSpeed, + rideSortElevation: ride.SortElevation, + rideSortCotacol: ride.SortCotacol, + rideSortCotacolKm: ride.SortCotacolKm, } column, found := columns[s.Column] return column, found @@ -277,7 +276,7 @@ func officialClimbCreateURL(id int64) string { return fmt.Sprintf("/rides/%d/official-climbs", id) } -func buildRideDetailView(item rides.Ride, parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideDetailView { +func buildRideDetailView(item ride.StoredRide, parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) RideDetailView { coordinates := make([][]float64, parsed.Len()) for i := 0; i < parsed.Len(); i++ { coordinate := parsed.Coord(i) -- cgit v1.3