Add segments, YTD stats, route matching fixes, body battery layout, pace fix
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 9s

- Segments page: new /segments route with auto-generate (1km splits, turn
  detection, hill detection), manual segment creation, per-segment performance
  times across matched activities; fixed auth on existing segment endpoints
- YTD distance: new /activities/stats/ytd endpoint; Dashboard replaces
  'Total distance' with 'Running this year' + 'Cycling this year'; Activities
  page shows YTD stats row
- Weekly chart click: clicking a Dashboard bar navigates to Activities filtered
  to that week; Activities reads from/to query params with dismissable chip
- Route matching: add ±2.5% distance gate + 3% relative DTW threshold
  (was flat 80m); tighten candidate pre-filter from 80/120% to 95/105%
- Body battery layout: HR chart and body battery now side-by-side at same
  height on large screens instead of stacked full-width
- Pace display fix: MetricTimeline clamps GPS speed outliers before computing
  Y-axis domain; tick formatter guards against v<=0 or v>25 m/s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:01:25 +01:00
parent f0bbe92b2c
commit 02eccad578
13 changed files with 797 additions and 32 deletions
+40 -3
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Link, useSearchParams, useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { format } from 'date-fns'
import api from '../utils/api'
import {
formatDuration, formatDistance, formatPace, formatHeartRate,
@@ -10,24 +11,38 @@ import {
const SPORTS = ['all', 'running', 'cycling', 'hiking', 'walking']
export default function ActivitiesPage() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const [sport, setSport] = useState('all')
const [page, setPage] = useState(1)
const fromParam = searchParams.get('from')
const toParam = searchParams.get('to')
const { data: activities, isLoading } = useQuery({
queryKey: ['activities', sport, page],
queryKey: ['activities', sport, page, fromParam, toParam],
queryFn: () =>
api.get('/activities/', {
params: {
sport_type: sport === 'all' ? undefined : sport,
page,
per_page: 20,
from_date: fromParam ? new Date(fromParam).toISOString() : undefined,
to_date: toParam ? new Date(toParam + 'T23:59:59').toISOString() : undefined,
},
}).then(r => r.data),
})
const { data: ytdStats } = useQuery({
queryKey: ['ytd-stats'],
queryFn: () => api.get('/activities/stats/ytd').then(r => r.data),
})
const clearDateFilter = () => navigate('/activities')
return (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold text-white">Activities</h1>
<Link
to="/upload"
@@ -37,6 +52,28 @@ export default function ActivitiesPage() {
</Link>
</div>
{/* YTD stats */}
{ytdStats && (
<div className="flex gap-4 mb-4 text-sm">
{ytdStats.running_km > 0 && (
<span className="text-blue-400">🏃 {ytdStats.running_km.toFixed(0)} km this year</span>
)}
{ytdStats.cycling_km > 0 && (
<span className="text-orange-400">🚴 {ytdStats.cycling_km.toFixed(0)} km this year</span>
)}
</div>
)}
{/* Date filter chip */}
{fromParam && (
<div className="flex items-center gap-2 mb-4">
<span className="text-xs bg-blue-600/20 text-blue-300 border border-blue-500/30 px-3 py-1 rounded-full">
Week of {format(new Date(fromParam), 'MMM d, yyyy')}
</span>
<button onClick={clearDateFilter} className="text-xs text-gray-500 hover:text-gray-300 transition-colors"> Clear</button>
</div>
)}
{/* Sport filter */}
<div className="flex gap-2 mb-6 flex-wrap">
{SPORTS.map(s => (