aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--cli/cli.go7
-rw-r--r--cli/cli_test.go12
-rw-r--r--ride/store.go (renamed from rides/rides.go)65
-rw-r--r--ride/store_test.go (renamed from rides/rides_test.go)19
-rw-r--r--web/ride-filters.go7
-rw-r--r--web/ride-filters_test.go23
-rw-r--r--web/rides.go17
-rw-r--r--web/server_test.go71
-rw-r--r--web/sync.go12
-rw-r--r--web/templates.templ2
-rw-r--r--web/views.go27
12 files changed, 134 insertions, 141 deletions
diff --git a/README.md b/README.md
index 9341208..b108792 100644
--- a/README.md
+++ b/README.md
@@ -61,10 +61,9 @@ Standard apps are rate-limited to 100 calls per 15 minutes and 1,000 per day.
## Architecture
- Go 1.27; SQLite via `mattn/go-sqlite3`
-- `ride` — GPX parsing, climb detection, difficulty scores (KOM + Cotacol), similarity index
+- `ride` — GPX parsing, climb detection, difficulty scores (KOM + Cotacol), and SQLite persistence for imported ride metadata and versioned computed values
- `mountain_pass` — OSM PBF extraction (`mountain_pass=yes` / `natural=mountain_pass` nodes), pass storage and querying, and crossings/climb matching
- `strava` — OAuth2 client returning activity metadata and GPX data
-- `rides` — SQLite persistence for imported ride metadata and versioned computed values
- `config` — typed YAML configuration and atomic persistence
- `web` — HTTP server, OAuth callback, sync orchestration, and templ pages
- `cli` — command handlers for data import, backfill, and benchmarking
@@ -75,10 +74,9 @@ flowchart TB
main["main (bootstrap: main.go)"]
- ride["ride (GPX parsing, climbs, Cotacol)"]
+ ride["ride (GPX parsing, climbs, Cotacol, persistence)"]
mpass["mountain_pass (OSM PBF, crossings)"]
strava["strava (OAuth, List, Get)"]
- rides["rides (SQLite persistence)"]
config["config (YAML)"]
web["web (HTTP + templ)"]
cli["cli (import, backfill, benchmark)"]
@@ -89,7 +87,7 @@ flowchart TB
cli --> web
mpass --> ride
web --> strava
- web --> rides
+ web --> ride
web --> config
```
@@ -111,9 +109,12 @@ TypeScript checks can also be run directly with `npm run check`, covering format
- http://localhost:8080/rides/368: does not contain Pont Rout
- http://localhost:8080/rides/6: Cat 4 inside Chemin des Lauriers (embrassé)
- Compute estimated power, speed
+- Plot de difficultés normalisées contre moving duration
+- Categorised rides like to-dos
+- Compact binary or vectorized storage for root
+- For a ride, scatter, one point per 100m segment. Slope vs speed
- Plot speed and slope per segment, colored by heart rate
- http://localhost:8080/rides/378: Repentance partial climb
-- regroup ride and rides packages
- screaming archi + hexagon
- Blog with pictures and markdown
- Road quality (Arbois = 2/5, Roquefavour = 4/5)
diff --git a/cli/cli.go b/cli/cli.go
index 52b5a8c..68a86a6 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -16,7 +16,6 @@ import (
"github.com/martinlehoux/biking_home/config"
"github.com/martinlehoux/biking_home/mountain_pass"
"github.com/martinlehoux/biking_home/ride"
- "github.com/martinlehoux/biking_home/rides"
"github.com/martinlehoux/biking_home/web"
"github.com/martinlehoux/kagamigo/kcore"
)
@@ -42,7 +41,7 @@ func Run(db *sql.DB, configPath string, appConfig config.Config) {
_, err := mountain_pass.ExtractMountainPasses(context.Background(), *extractOSM, db)
kcore.Expect(err, "failed to extract mountain passes from OSM")
case *backfill:
- count, err := rides.Backfill(db)
+ count, err := ride.Backfill(db)
kcore.Expect(err, "failed to backfill ride values")
slog.Info("Backfilled ride values", "rides", count)
case *backtest:
@@ -67,7 +66,7 @@ type backtestSample struct {
}
func runBacktest(db *sql.DB) error {
- items, err := rides.List(db)
+ items, err := ride.List(db)
if err != nil {
return err
}
@@ -128,7 +127,7 @@ func runBacktest(db *sql.DB) error {
return nil
}
-func isBacktestEligible(item rides.Ride) bool {
+func isBacktestEligible(item ride.StoredRide) bool {
return item.GPXPath != "" &&
item.DistanceM >= backtestMinimumDistanceM &&
item.ExternalMovingTimeS > 0 &&
diff --git a/cli/cli_test.go b/cli/cli_test.go
index 7964387..3e2afe2 100644
--- a/cli/cli_test.go
+++ b/cli/cli_test.go
@@ -3,22 +3,22 @@ package cli
import (
"testing"
- "github.com/martinlehoux/biking_home/rides"
+ "github.com/martinlehoux/biking_home/ride"
"github.com/stretchr/testify/assert"
)
func TestBacktestEligibilityExcludesShortDistances(t *testing.T) {
- short := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000 - 1, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
- minimum := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
+ short := ride.StoredRide{GPXPath: "ride.gpx", DistanceM: 10_000 - 1, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
+ minimum := ride.StoredRide{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
assert.False(t, isBacktestEligible(short))
assert.True(t, isBacktestEligible(minimum))
}
func TestBacktestEligibilityExcludesLowMovingRatio(t *testing.T) {
- low := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 7_999, ElapsedTimeS: 10_000}
- boundary := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
- noElapsed := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 1}
+ low := ride.StoredRide{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 7_999, ElapsedTimeS: 10_000}
+ boundary := ride.StoredRide{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 8_000, ElapsedTimeS: 10_000}
+ noElapsed := ride.StoredRide{GPXPath: "ride.gpx", DistanceM: 10_000, ExternalMovingTimeS: 1}
assert.False(t, isBacktestEligible(low))
assert.True(t, isBacktestEligible(boundary))
diff --git a/rides/rides.go b/ride/store.go
index 94fbc64..af598e8 100644
--- a/rides/rides.go
+++ b/ride/store.go
@@ -1,4 +1,4 @@
-package rides
+package ride
import (
"database/sql"
@@ -6,11 +6,10 @@ import (
"log/slog"
"time"
- "github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/kagamigo/kcore"
)
-type Ride struct {
+type StoredRide struct {
ID int64
ExternalID string
GPXPath string
@@ -45,20 +44,20 @@ const (
const columns = "id, external_id, gpx_path, name, type, start_date, distance_m, external_moving_time_s, moving_time_s, moving_time_algo_version, elapsed_time_s, total_elevation_gain_m, average_speed_mps, cotacol_score, cotacol_algo_version, created_at, updated_at"
-func Save(db *sql.DB, r Ride) error {
+func Save(db *sql.DB, r StoredRide) error {
var cotacolScore any
var cotacolAlgorithmVersion any
var movingTimeS any
var movingTimeAlgorithmVersion any
if r.GPXPath != "" {
- parsed, err := ride.ParseFile(ride.GPXRideParser{}, r.GPXPath)
+ parsed, err := ParseFile(GPXRideParser{}, r.GPXPath)
if err != nil {
return fmt.Errorf("compute ride values for %q: %w", r.ExternalID, err)
}
- cotacolScore = ride.Cotacol(parsed)
- cotacolAlgorithmVersion = ride.CotacolAlgorithmVersion
- movingTimeS = int64(ride.MovingTime(parsed) / time.Second)
- movingTimeAlgorithmVersion = ride.MovingTimeAlgorithmVersion
+ cotacolScore = Cotacol(parsed)
+ cotacolAlgorithmVersion = CotacolAlgorithmVersion
+ movingTimeS = int64(MovingTime(parsed) / time.Second)
+ movingTimeAlgorithmVersion = MovingTimeAlgorithmVersion
}
_, err := db.Exec(`
INSERT INTO rides (external_id, gpx_path, name, type, start_date, distance_m, external_moving_time_s, moving_time_s, moving_time_algo_version, elapsed_time_s, total_elevation_gain_m, average_speed_mps, cotacol_score, cotacol_algo_version)
@@ -87,7 +86,7 @@ func Backfill(db *sql.DB) (int, error) {
if err != nil {
return 0, err
}
- var pending []Ride
+ var pending []StoredRide
for rows.Next() {
item, err := scanRide(rows)
if err != nil {
@@ -114,11 +113,11 @@ func Backfill(db *sql.DB) (int, error) {
return backfilled, nil
}
-func List(db *sql.DB) ([]Ride, error) {
+func List(db *sql.DB) ([]StoredRide, error) {
return ListSorted(db, SortStartDate, true)
}
-func ListSorted(db *sql.DB, column SortColumn, descending bool) ([]Ride, error) {
+func ListSorted(db *sql.DB, column SortColumn, descending bool) ([]StoredRide, error) {
expression, ok := sortExpressions[column]
if !ok {
return nil, fmt.Errorf("invalid ride sort column %q", column)
@@ -130,10 +129,10 @@ func ListSorted(db *sql.DB, column SortColumn, descending bool) ([]Ride, error)
query := "SELECT " + columns + " FROM rides ORDER BY "
args := []any{}
if column == SortMovingTime {
- args = append(args, ride.MovingTimeAlgorithmVersion)
+ args = append(args, MovingTimeAlgorithmVersion)
} else if isComputedSort(column) {
query += "(cotacol_score IS NULL OR cotacol_algo_version IS NULL OR cotacol_algo_version <> ? OR distance_m <= 0) ASC, "
- args = append(args, ride.CotacolAlgorithmVersion)
+ args = append(args, CotacolAlgorithmVersion)
}
query += expression + " " + direction + ", id DESC"
rows, err := db.Query(query, args...)
@@ -141,7 +140,7 @@ func ListSorted(db *sql.DB, column SortColumn, descending bool) ([]Ride, error)
return nil, err
}
defer rows.Close()
- var rides []Ride
+ var rides []StoredRide
for rows.Next() {
ride, err := scanRide(rows)
if err != nil {
@@ -167,26 +166,26 @@ func isComputedSort(column SortColumn) bool {
return column == SortCotacol || column == SortCotacolKm
}
-func GetByExternalID(db *sql.DB, externalID string) (Ride, bool, error) {
+func GetByExternalID(db *sql.DB, externalID string) (StoredRide, bool, error) {
row := db.QueryRow("SELECT "+columns+" FROM rides WHERE external_id = ?", externalID)
ride, err := scanRide(row)
if err == sql.ErrNoRows {
- return Ride{}, false, nil
+ return StoredRide{}, false, nil
}
if err != nil {
- return Ride{}, false, err
+ return StoredRide{}, false, err
}
return ride, true, nil
}
-func GetByID(db *sql.DB, id int64) (Ride, bool, error) {
+func GetByID(db *sql.DB, id int64) (StoredRide, bool, error) {
row := db.QueryRow("SELECT "+columns+" FROM rides WHERE id = ?", id)
ride, err := scanRide(row)
if err == sql.ErrNoRows {
- return Ride{}, false, nil
+ return StoredRide{}, false, nil
}
if err != nil {
- return Ride{}, false, err
+ return StoredRide{}, false, err
}
return ride, true, nil
}
@@ -195,9 +194,9 @@ type scanner interface {
Scan(dest ...any) error
}
-func scanRide(s scanner) (Ride, error) {
+func scanRide(s scanner) (StoredRide, error) {
var (
- ride Ride
+ ride StoredRide
startDate string
movingTimeS sql.NullInt64
movingTimeAlgoVer sql.NullString
@@ -208,7 +207,7 @@ func scanRide(s scanner) (Ride, error) {
)
err := s.Scan(&ride.ID, &ride.ExternalID, &ride.GPXPath, &ride.Name, &ride.Type, &startDate, &ride.DistanceM, &ride.ExternalMovingTimeS, &movingTimeS, &movingTimeAlgoVer, &ride.ElapsedTimeS, &ride.TotalElevationGainM, &ride.AverageSpeedMps, &cotacolScore, &cotacolAlgoVersion, &createdAt, &updatedAt)
if err != nil {
- return Ride{}, err
+ return StoredRide{}, err
}
if movingTimeS.Valid {
ride.movingTimeS = &movingTimeS.Int64
@@ -224,44 +223,44 @@ func scanRide(s scanner) (Ride, error) {
}
ride.StartDate, err = time.Parse(time.RFC3339, startDate)
if err != nil {
- return Ride{}, kcore.Wrap(err, "invalid start_date in rides row")
+ return StoredRide{}, kcore.Wrap(err, "invalid start_date in rides row")
}
ride.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
if err != nil {
- return Ride{}, kcore.Wrap(err, "invalid created_at in rides row")
+ return StoredRide{}, kcore.Wrap(err, "invalid created_at in rides row")
}
ride.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt)
if err != nil {
- return Ride{}, kcore.Wrap(err, "invalid updated_at in rides row")
+ return StoredRide{}, kcore.Wrap(err, "invalid updated_at in rides row")
}
return ride, nil
}
-func (r Ride) CotacolAlgorithmVersion() string {
+func (r StoredRide) CotacolAlgorithmVersion() string {
return r.cotacolAlgoVersion
}
-func (r Ride) MovingTimeS() (int64, bool) {
+func (r StoredRide) MovingTimeS() (int64, bool) {
if r.movingTimeS == nil {
return 0, false
}
return *r.movingTimeS, true
}
-func (r Ride) MovingTimeAlgorithmVersion() string {
+func (r StoredRide) MovingTimeAlgorithmVersion() string {
return r.movingTimeAlgoVersion
}
// EffectiveMovingTimeS is the canonical moving time when the stored value
// matches the current algorithm version, falling back to Strava's value.
-func (r Ride) EffectiveMovingTimeS() int64 {
- if moving, ok := r.MovingTimeS(); ok && r.movingTimeAlgoVersion == ride.MovingTimeAlgorithmVersion {
+func (r StoredRide) EffectiveMovingTimeS() int64 {
+ if moving, ok := r.MovingTimeS(); ok && r.movingTimeAlgoVersion == MovingTimeAlgorithmVersion {
return moving
}
return r.ExternalMovingTimeS
}
-func (r Ride) CotacolScore() (float64, bool) {
+func (r StoredRide) CotacolScore() (float64, bool) {
if r.cotacolScore == nil {
return 0, false
}
diff --git a/rides/rides_test.go b/ride/store_test.go
index ea87172..877f4e5 100644
--- a/rides/rides_test.go
+++ b/ride/store_test.go
@@ -1,4 +1,4 @@
-package rides
+package ride
import (
"database/sql"
@@ -8,7 +8,6 @@ import (
"time"
"github.com/martinlehoux/biking_home/internal/dbtest"
- "github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,11 +17,11 @@ func newTestDB(t *testing.T) *sql.DB {
return dbtest.New(t)
}
-func sampleRide(t *testing.T) Ride {
+func sampleRide(t *testing.T) StoredRide {
t.Helper()
gpxPath := filepath.Join(t.TempDir(), "ride.gpx")
require.NoError(t, os.WriteFile(gpxPath, []byte(testGPX), 0o600))
- return Ride{
+ return StoredRide{
ExternalID: "strava:1234",
GPXPath: gpxPath,
Name: "Morning Ride",
@@ -54,7 +53,7 @@ func TestUpsertAndGet(t *testing.T) {
score, found := got.CotacolScore()
require.True(t, found)
assert.Greater(t, score, 0.0)
- assert.Equal(t, ride.CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
+ assert.Equal(t, CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
_, ok, err = GetByExternalID(db, "strava:9999")
require.NoError(t, err)
@@ -90,7 +89,7 @@ func TestSaveComputesCotacol(t *testing.T) {
score, found := got.CotacolScore()
require.True(t, found)
assert.Greater(t, score, 0.0)
- assert.Equal(t, ride.CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
+ assert.Equal(t, CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
}
func TestSaveComputesMovingTimeAndPreservesExternal(t *testing.T) {
@@ -103,7 +102,7 @@ func TestSaveComputesMovingTimeAndPreservesExternal(t *testing.T) {
moving, found := got.MovingTimeS()
require.True(t, found)
assert.Equal(t, int64(60), moving)
- assert.Equal(t, ride.MovingTimeAlgorithmVersion, got.MovingTimeAlgorithmVersion())
+ assert.Equal(t, MovingTimeAlgorithmVersion, got.MovingTimeAlgorithmVersion())
assert.Equal(t, int64(7_200), got.ExternalMovingTimeS)
}
@@ -191,7 +190,7 @@ func TestBackfill(t *testing.T) {
require.True(t, ok)
_, found := got.CotacolScore()
assert.True(t, found)
- assert.Equal(t, ride.CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
+ assert.Equal(t, CotacolAlgorithmVersion, got.CotacolAlgorithmVersion())
count, err = Backfill(db)
require.NoError(t, err)
@@ -264,7 +263,7 @@ func TestListSortedByCotacolKeepsStaleLast(t *testing.T) {
UPDATE rides
SET cotacol_score = CASE external_id WHEN ? THEN 4 ELSE 6 END,
cotacol_algo_version = ?
- `, first.ExternalID, ride.CotacolAlgorithmVersion)
+ `, first.ExternalID, CotacolAlgorithmVersion)
require.NoError(t, err)
items, err := ListSorted(db, SortCotacol, false)
@@ -294,7 +293,7 @@ func TestListSortedByCotacolPer100Km(t *testing.T) {
UPDATE rides
SET cotacol_score = CASE external_id WHEN ? THEN 4 ELSE 6 END,
cotacol_algo_version = ?
- `, first.ExternalID, ride.CotacolAlgorithmVersion)
+ `, first.ExternalID, CotacolAlgorithmVersion)
require.NoError(t, err)
items, err := ListSorted(db, SortCotacolKm, false)
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 = `<?xml version="1.0"?><gpx xmlns="http://www.topog
func TestHandlerRendersRidesPage(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:1234",
GPXPath: testGPXPath(t, "long.gpx"),
Name: "Long Ride",
@@ -81,7 +80,7 @@ func TestHandlerRendersRidesPage(t *testing.T) {
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 20_000,
}))
- require.NoError(t, rides.Save(db, rides.Ride{
+ require.NoError(t, ride.Save(db, ride.StoredRide{
ExternalID: "strava:5678",
GPXPath: testGPXPath(t, "short.gpx"),
Name: "Short Ride",
@@ -106,7 +105,7 @@ func TestHandlerRendersRidesPage(t *testing.T) {
func TestHandlerRendersRideDetailWithEmbeddedRoute(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:550e8400-e29b-41d4-a716-446655440000",
GPXPath: testGPXPath(t, "detail.gpx"),
Name: "Detailed Ride",
@@ -122,7 +121,7 @@ func TestHandlerRendersRideDetailWithEmbeddedRoute(t *testing.T) {
2, "Distant pass", 500, 43.2, 5.2,
)
require.NoError(t, err)
- item, found, err := rides.GetByExternalID(db, "strava:550e8400-e29b-41d4-a716-446655440000")
+ item, found, err := ride.GetByExternalID(db, "strava:550e8400-e29b-41d4-a716-446655440000")
require.NoError(t, err)
require.True(t, found)
@@ -268,7 +267,7 @@ func TestHandlerCreatesOfficialClimbFromRoutePoints(t *testing.T) {
server, db := newWebTestServer(t)
routePath := filepath.Join(t.TempDir(), "official-climb.gpx")
require.NoError(t, os.WriteFile(routePath, []byte(officialClimbTrackGPX), 0o600))
- require.NoError(t, rides.Save(db, rides.Ride{
+ require.NoError(t, ride.Save(db, ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000",
GPXPath: routePath,
Name: "Climb Candidate",
@@ -276,7 +275,7 @@ func TestHandlerCreatesOfficialClimbFromRoutePoints(t *testing.T) {
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 2_000,
}))
- item, found, err := rides.GetByExternalID(db, "strava:550e8400-e29b-41d4-a716-446655440000")
+ item, found, err := ride.GetByExternalID(db, "strava:550e8400-e29b-41d4-a716-446655440000")
require.NoError(t, err)
require.True(t, found)
@@ -325,15 +324,15 @@ func TestHandlerCreatesOfficialClimbFromRoutePoints(t *testing.T) {
func TestHandlerRendersRideDetailWithoutRoute(t *testing.T) {
server, db := newWebTestServer(t)
- item := rides.Ride{
+ item := ride.StoredRide{
ExternalID: "strava:14701658670",
Name: "Indoor Ride",
Type: "VirtualRide",
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 20_000,
}
- require.NoError(t, rides.Save(db, item))
- stored, found, err := rides.GetByExternalID(db, item.ExternalID)
+ require.NoError(t, ride.Save(db, item))
+ stored, found, err := ride.GetByExternalID(db, item.ExternalID)
require.NoError(t, err)
require.True(t, found)
@@ -350,22 +349,22 @@ func TestHandlerRendersRideDetailWithoutRoute(t *testing.T) {
func TestHandlerRendersAnalyticsChart(t *testing.T) {
server, db := newWebTestServer(t)
- first := rides.Ride{
+ first := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440001",
Name: "Fastish Ride",
Type: "Ride",
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 40_000,
}
- second := rides.Ride{
+ second := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440002",
Name: "Climbing Ride",
Type: "Ride",
StartDate: time.Date(2026, 8, 2, 7, 0, 0, 0, time.UTC),
DistanceM: 40_000,
}
- require.NoError(t, rides.Save(db, first))
- require.NoError(t, rides.Save(db, second))
+ require.NoError(t, ride.Save(db, first))
+ require.NoError(t, ride.Save(db, second))
_, err := db.Exec(`
UPDATE rides
SET cotacol_score = CASE external_id WHEN ? THEN 20 ELSE 120 END,
@@ -392,22 +391,22 @@ func TestHandlerRendersAnalyticsChart(t *testing.T) {
func TestHandlerFiltersAnalyticsByDistance(t *testing.T) {
server, db := newWebTestServer(t)
- short := rides.Ride{
+ short := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440003",
Name: "Short Ride",
Type: "Ride",
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 40_000,
}
- long := rides.Ride{
+ long := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440004",
Name: "Long Ride",
Type: "Ride",
StartDate: time.Date(2026, 8, 2, 7, 0, 0, 0, time.UTC),
DistanceM: 100_000,
}
- require.NoError(t, rides.Save(db, short))
- require.NoError(t, rides.Save(db, long))
+ require.NoError(t, ride.Save(db, short))
+ require.NoError(t, ride.Save(db, long))
_, err := db.Exec(`
UPDATE rides
SET cotacol_score = CASE external_id WHEN ? THEN 20 ELSE 120 END,
@@ -448,7 +447,7 @@ func TestHandlerShowsErrorForUnavailableRideRoute(t *testing.T) {
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)
- item, found, err := rides.GetByExternalID(db, "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8")
+ item, found, err := ride.GetByExternalID(db, "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8")
require.NoError(t, err)
require.True(t, found)
@@ -567,10 +566,10 @@ func TestSyncRidesContinuesAfterInvalidActivity(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, SyncProgress{Total: 2, Completed: 2, Imported: 1, Skipped: 1}, progress)
assert.Len(t, updates, 3)
- _, found, err := rides.GetByExternalID(db, "strava:14701658670")
+ _, found, err := ride.GetByExternalID(db, "strava:14701658670")
require.NoError(t, err)
assert.False(t, found)
- _, found, err = rides.GetByExternalID(db, "strava:14701658671")
+ _, found, err = ride.GetByExternalID(db, "strava:14701658671")
require.NoError(t, err)
assert.True(t, found)
assert.NoFileExists(t, filepath.Join(gpxDir, "activity_14701658670.gpx"))
@@ -599,14 +598,14 @@ func TestSyncRidesSavesActivityWithoutRoute(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, SyncProgress{Total: 2, Completed: 2, Imported: 2}, progress)
- indoor, found, err := rides.GetByExternalID(db, "strava:14701658670")
+ indoor, found, err := ride.GetByExternalID(db, "strava:14701658670")
require.NoError(t, err)
require.True(t, found)
assert.Empty(t, indoor.GPXPath)
_, ready := indoor.CotacolScore()
assert.False(t, ready)
assert.NoFileExists(t, staleGPXPath)
- _, found, err = rides.GetByExternalID(db, "strava:14701658671")
+ _, found, err = ride.GetByExternalID(db, "strava:14701658671")
require.NoError(t, err)
assert.True(t, found)
}
@@ -712,7 +711,7 @@ func TestRideSortHeadersToggleDirection(t *testing.T) {
func TestBuildRideViewsUsesStoredCotacol(t *testing.T) {
_, db := newWebTestServer(t)
- ride := rides.Ride{
+ stored := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000",
GPXPath: testGPXPath(t, "stored.gpx"),
Name: "Stored Ride",
@@ -720,10 +719,10 @@ func TestBuildRideViewsUsesStoredCotacol(t *testing.T) {
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 20_000,
}
- require.NoError(t, rides.Save(db, ride))
- _, err := db.Exec("UPDATE rides SET gpx_path = ? WHERE external_id = ?", "missing.gpx", ride.ExternalID)
+ require.NoError(t, ride.Save(db, stored))
+ _, err := db.Exec("UPDATE rides SET gpx_path = ? WHERE external_id = ?", "missing.gpx", stored.ExternalID)
require.NoError(t, err)
- items, err := rides.List(db)
+ items, err := ride.List(db)
require.NoError(t, err)
views := buildRideViews(items)
@@ -733,7 +732,7 @@ func TestBuildRideViewsUsesStoredCotacol(t *testing.T) {
func TestBuildRideViewMovingTimePrefersCanonical(t *testing.T) {
_, db := newWebTestServer(t)
- ride := rides.Ride{
+ stored := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000",
GPXPath: testGPXPath(t, "moving.gpx"),
Name: "Moving Ride",
@@ -742,8 +741,8 @@ func TestBuildRideViewMovingTimePrefersCanonical(t *testing.T) {
DistanceM: 20_000,
ExternalMovingTimeS: 7_200,
}
- require.NoError(t, rides.Save(db, ride))
- items, err := rides.List(db)
+ require.NoError(t, ride.Save(db, stored))
+ items, err := ride.List(db)
require.NoError(t, err)
require.Len(t, items, 1)
@@ -752,7 +751,7 @@ func TestBuildRideViewMovingTimePrefersCanonical(t *testing.T) {
func TestHandlerSortsRidesByDistance(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:550e8400-e29b-41d4-a716-446655440000",
GPXPath: testGPXPath(t, "near.gpx"),
Name: "Near Ride",
@@ -760,7 +759,7 @@ func TestHandlerSortsRidesByDistance(t *testing.T) {
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 20_000,
}))
- require.NoError(t, rides.Save(db, rides.Ride{
+ require.NoError(t, ride.Save(db, ride.StoredRide{
ExternalID: "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8",
GPXPath: testGPXPath(t, "far.gpx"),
Name: "Far Ride",
@@ -781,7 +780,7 @@ func TestHandlerSortsRidesByDistance(t *testing.T) {
func TestHandlerSortsRidesByCotacol(t *testing.T) {
server, db := newWebTestServer(t)
- first := rides.Ride{
+ first := ride.StoredRide{
ExternalID: "strava:550e8400-e29b-41d4-a716-446655440000",
GPXPath: testGPXPath(t, "first.gpx"),
Name: "Low Cotacol",
@@ -789,7 +788,7 @@ func TestHandlerSortsRidesByCotacol(t *testing.T) {
StartDate: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC),
DistanceM: 20_000,
}
- second := rides.Ride{
+ second := ride.StoredRide{
ExternalID: "strava:6ba7b810-9dad-41d1-80b4-00c04fd430c8",
GPXPath: testGPXPath(t, "second.gpx"),
Name: "High Cotacol",
@@ -797,8 +796,8 @@ func TestHandlerSortsRidesByCotacol(t *testing.T) {
StartDate: time.Date(2026, 8, 2, 7, 0, 0, 0, time.UTC),
DistanceM: 40_000,
}
- require.NoError(t, rides.Save(db, first))
- require.NoError(t, rides.Save(db, second))
+ require.NoError(t, ride.Save(db, first))
+ require.NoError(t, ride.Save(db, second))
_, err := db.Exec(`
UPDATE rides
SET cotacol_score = CASE external_id WHEN ? THEN 1 ELSE 4 END
diff --git a/web/sync.go b/web/sync.go
index 4a6ba5f..b491ee0 100644
--- a/web/sync.go
+++ b/web/sync.go
@@ -12,7 +12,7 @@ import (
"strings"
"time"
- "github.com/martinlehoux/biking_home/rides"
+ "github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/biking_home/strava"
"github.com/martinlehoux/kagamigo/kcore"
)
@@ -104,7 +104,7 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time,
}
for _, summary := range activities {
externalID := fmt.Sprintf("strava:%d", summary.ID)
- _, exists, err := rides.GetByExternalID(s.db, externalID)
+ _, exists, err := ride.GetByExternalID(s.db, externalID)
if err != nil {
return progress, err
}
@@ -120,7 +120,7 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time,
if err != nil {
if errors.Is(err, strava.ErrNoTrackPoints) {
_ = os.Remove(filepath.Join(gpxDir, fmt.Sprintf("activity_%d.gpx", summary.ID)))
- if saveErr := rides.Save(s.db, rideFromActivity(externalID, "", activity)); saveErr != nil {
+ if saveErr := ride.Save(s.db, rideFromActivity(externalID, "", activity)); saveErr != nil {
if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("save metadata: %w", saveErr)); reportErr != nil {
return progress, reportErr
}
@@ -147,7 +147,7 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time,
}
continue
}
- if err := rides.Save(s.db, rideFromActivity(externalID, gpxPath, activity)); err != nil {
+ if err := ride.Save(s.db, rideFromActivity(externalID, gpxPath, activity)); err != nil {
_ = os.Remove(gpxPath)
if reportErr := skipSyncActivity(&progress, report, summary, fmt.Errorf("save activity: %w", err)); reportErr != nil {
return progress, reportErr
@@ -164,12 +164,12 @@ func (s *Server) syncRides(client syncClient, gpxDir string, from, to time.Time,
return progress, nil
}
-func rideFromActivity(externalID, gpxPath string, activity strava.Activity) rides.Ride {
+func rideFromActivity(externalID, gpxPath string, activity strava.Activity) ride.StoredRide {
activityType := activity.SportType
if activityType == "" {
activityType = activity.Type
}
- return rides.Ride{
+ return ride.StoredRide{
ExternalID: externalID,
GPXPath: gpxPath,
Name: activity.Name,
diff --git a/web/templates.templ b/web/templates.templ
index f2b62f8..0660d07 100644
--- a/web/templates.templ
+++ b/web/templates.templ
@@ -84,7 +84,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">{ item.MovingTime }</td><td class="numeric">{ formatSpeed(item.AverageSpeedMps) }</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.Ride.ID)) }>{ item.Ride.Name }</a></strong><br/><small>{ item.Ride.Type }</small></td><td>{ formatRideDate(item.Ride.StartDate) }</td><td class="numeric">{ formatDistance(item.Ride.DistanceM) }</td><td class="numeric">{ item.MovingTime }</td><td class="numeric">{ formatSpeed(item.Ride.AverageSpeedMps) }</td><td class="numeric">{ formatElevation(item.Ride.TotalElevationGainM) }</td><td class="numeric">{ item.Cotacol }</td><td class="numeric">{ item.CotacolPer100Km }</td></tr>
}
</tbody>
</table>
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)