From ed9516cbcb3c5254980a9e1dba7d8fa5ea230b35 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Fri, 21 Aug 2026 19:30:50 +0200 Subject: Add distance filter to analytics and go fix to the check gate --- web/rides.go | 22 ++++++++++++++++++++-- web/server_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ web/static/style.css | 2 ++ web/templates.templ | 25 +++++++++++++++++++++++-- web/views.go | 5 +++++ 5 files changed, 91 insertions(+), 4 deletions(-) (limited to 'web') diff --git a/web/rides.go b/web/rides.go index aa39d69..cb38b3c 100644 --- a/web/rides.go +++ b/web/rides.go @@ -94,15 +94,33 @@ func (s *Server) handleAnalytics(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to load rides", http.StatusInternalServerError) return } + filters := parseRideFilters(r.URL.Query()) + minKm := minimumDisplayedDistanceM / 1000 + if filters.DistanceMinKm != nil && *filters.DistanceMinKm > minKm { + minKm = *filters.DistanceMinKm + } + maxKm := 0 + if filters.DistanceMaxKm != nil { + maxKm = *filters.DistanceMaxKm + } eligible := make([]rides.Ride, 0, len(items)) for _, item := range items { - if item.DistanceM >= minimumDisplayedDistanceM { - eligible = append(eligible, item) + distanceKm := int(item.DistanceM / 1000) + if distanceKm < minKm || (maxKm != 0 && distanceKm > maxKm) { + continue } + eligible = append(eligible, item) + } + filterView := RideFilterView{ + DistanceMinKm: rideFilterValue(filters.DistanceMinKm, rideFilterDistanceMinKm), + DistanceMaxKm: rideFilterValue(filters.DistanceMaxKm, rideFilterDistanceMaxKm), + Active: filters.Active(), + ResetURL: "/analytics", } slog.Info("Loaded ride analytics", "rides", len(eligible)) kcore.RenderPage(r.Context(), AnalyticsPage(AnalyticsPageData{ Points: buildAnalyticsPoints(eligible), + Filter: filterView, }), w) } diff --git a/web/server_test.go b/web/server_test.go index 42b85b1..0514dfc 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -390,6 +390,47 @@ func TestHandlerRendersAnalyticsChart(t *testing.T) { assert.Contains(t, body, `href="/analytics"`) } +func TestHandlerFiltersAnalyticsByDistance(t *testing.T) { + server, db := newWebTestServer(t) + short := rides.Ride{ + 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{ + 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)) + _, err := db.Exec(` + UPDATE rides + SET cotacol_score = CASE external_id WHEN ? THEN 20 ELSE 120 END, + cotacol_algo_version = ?, + average_speed_mps = CASE external_id WHEN ? THEN 7.0 ELSE 5.0 END + `, short.ExternalID, ride.CotacolAlgorithmVersion, long.ExternalID) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/analytics?distance_min=60", nil) + response := httptest.NewRecorder() + server.Handler().ServeHTTP(response, req) + + body := response.Body.String() + assert.Equal(t, http.StatusOK, response.Code) + assert.Contains(t, body, `id="analytics-chart"`) + assert.Contains(t, body, `id="analytics-points"`) + assert.Contains(t, body, `"cotacolPer100Km":120`) + assert.NotContains(t, body, `"cotacolPer100Km":50`) + assert.Contains(t, body, `name="distance_min"`) + assert.Contains(t, body, `href="/analytics"`) + assert.Contains(t, body, `data-analytics-count`) +} + func TestHandlerReturnsNotFoundForUnknownRide(t *testing.T) { server, _ := newWebTestServer(t) req := httptest.NewRequest(http.MethodGet, "/rides/999999", nil) diff --git a/web/static/style.css b/web/static/style.css index 55d6549..830e50a 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -90,6 +90,8 @@ th { color: var(--color-subtle); font-size: .78rem; letter-spacing: .08em; text- .ride-filters { display: grid; gap: 1.25rem; margin-top: 1.25rem; } .ride-filters > .section-heading { flex-wrap: wrap; } .ride-filter-ranges { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem 1.5rem; } +.analytics-filters-panel { margin-top: 1.25rem; } +.analytics-filters-panel .ride-filter-ranges { grid-template-columns: minmax(0, 1fr); max-width: 18rem; } .ride-filter-group { min-width: 0; margin: 0; padding: 0; border: 0; } .ride-filter-group legend { padding: 0; color: var(--color-forest); font-size: .9rem; font-weight: 750; } .range-values { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; margin-top: .45rem; color: var(--color-muted); font-size: .85rem; } diff --git a/web/templates.templ b/web/templates.templ index 71df20b..f2b62f8 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -101,16 +101,37 @@ templ AnalyticsContent(data AnalyticsPageData) {

Analytics

Ride analytics

One point per ride: average speed on the x axis, Cotacol per 100 km on the y axis.

+
+
+
+

Refine

Analyses

+
Reset
+
+
+ @RideRangeFilter(rideDistanceFilterConfig(data.Filter)) +
+
+
-

Chart

Speed vs. Cotacol per 100 km

+
+

Chart

Speed vs. Cotacol per 100 km

+ if len(data.Points) > 0 { + { formatRideCount(len(data.Points)) } + } +
if len(data.Points) == 0 { -
No eligible rides yet. Import rides to see the chart.
+ if data.Filter.Active { +
No rides match the selected filters. Clear filters.
+ } else { +
No eligible rides yet. Import rides to see the chart.
+ } } else {
Hover a point to inspect a ride. }
@templ.JSONScript("analytics-points", data.Points) + } diff --git a/web/views.go b/web/views.go index 9771a4d..7a5a461 100644 --- a/web/views.go +++ b/web/views.go @@ -28,6 +28,7 @@ type RidesPageData struct { type AnalyticsPageData struct { Points []AnalyticsPoint + Filter RideFilterView } type AnalyticsPoint struct { @@ -427,6 +428,10 @@ func formatClimbCount(count int) string { return fmt.Sprintf("%d", count) } +func formatRideCount(count int) string { + return fmt.Sprintf("%d", count) +} + func formatProfileDistance(distanceKm float64) string { return fmt.Sprintf("%.1f km", distanceKm) } -- cgit v1.3