Batch 1: dashboard, maps, segments rewrite, health, sync UX
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 9s

Fixes:
- Dashboard: featured most-recent activity card with map + stats
- Maps default to Street; preferCanvas + larger tile buffer for smoother pan/zoom
- Running cadence as colour-banded dots + 165 spm guide line
- Routes: inline row expansion, rename (PATCH /routes/{id}), podium + deltas, tiled map
- Records: remove reversed pace Y-axis
- Profile: remove resting HR; add goal weight
- Health: snapshot weight carry-forward; VO2 trend axis 30-70;
  weight goal line + kg/st-lb toggle + axis max; sleep 8h/avg lines
- Garmin sync progress moved to global store with persistent floating bar

Features:
- Speed-coloured activity route (default) with Speed/Solid toggle
- GPS-geometry segments: draw on map, match across all activities,
  1st/2nd/3rd leaderboard + podium badges (replaces old distance segments)
- Lap bests: best time per lap across a route + delta column
- Body Battery: highlight activity time windows

Schema: users.goal_weight_kg ALTER; new segments/segment_efforts tables.
Removes RouteSegment, the Segments page, and segment-bests endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 19:59:06 +01:00
parent e5feeb1178
commit bc437cce92
24 changed files with 1339 additions and 1445 deletions
+43 -1
View File
@@ -4,11 +4,21 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContaine
import { startOfWeek, format, subWeeks, eachWeekOfInterval, subDays, addDays } from 'date-fns'
import api from '../utils/api'
import StatCard from '../components/ui/StatCard'
import ActivityMap from '../components/activity/ActivityMap'
import {
formatDuration, formatDistance, formatPace, formatHeartRate,
formatDuration, formatDistance, formatPace, formatHeartRate, formatElevation,
formatDate, sportIcon, formatSleep,
} from '../utils/format'
function Stat({ label, value }) {
return (
<div className="bg-gray-900 px-4 py-3 flex flex-col justify-center">
<p className="text-xs text-gray-500">{label}</p>
<p className="text-lg font-semibold text-white">{value}</p>
</div>
)
}
function bbLevelColor(level) {
if (level == null) return '#6b7280'
if (level >= 75) return '#3b82f6'
@@ -154,6 +164,7 @@ export default function DashboardPage() {
})
const latest = healthSummary?.latest
const featured = recentActivities?.[0]
return (
<div className="p-6 space-y-6">
@@ -203,6 +214,37 @@ export default function DashboardPage() {
</div>
</div>
{/* Featured most-recent activity */}
{featured && (
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-800">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xl">{sportIcon(featured.sport_type)}</span>
<div className="min-w-0">
<Link to={`/activities/${featured.id}`} className="text-sm font-semibold text-white hover:text-blue-400 transition-colors truncate block">
{featured.name}
</Link>
<p className="text-xs text-gray-500">{formatDate(featured.start_time)}</p>
</div>
</div>
<Link to={`/activities/${featured.id}`} className="text-xs text-blue-400 hover:underline flex-shrink-0">Open </Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3">
<div className="lg:col-span-2 h-64 bg-gray-950">
{featured.polyline
? <ActivityMap polyline={featured.polyline} sportType={featured.sport_type} colorMode="solid" />
: <div className="flex items-center justify-center h-full text-gray-600 text-sm">No GPS track</div>}
</div>
<div className="grid grid-cols-2 lg:grid-cols-1 gap-px bg-gray-800/50">
<Stat label="Distance" value={formatDistance(featured.distance_m)} />
<Stat label="Elevation ↑" value={formatElevation(featured.elevation_gain_m)} />
<Stat label="Moving time" value={formatDuration(featured.duration_s)} />
<Stat label="Calories" value={featured.calories ? `${Math.round(featured.calories)} kcal` : '--'} />
</div>
</div>
</div>
)}
{/* Recent activities */}
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<div className="flex items-center justify-between mb-4">