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
+37 -19
View File
@@ -2,8 +2,9 @@ import { formatDuration, formatDistance, formatPace, formatHeartRate, formatCade
const RUNNING_TYPES = new Set(['running', 'hiking', 'walking'])
export default function LapTable({ laps, sportType }) {
export default function LapTable({ laps, sportType, lapBests }) {
const showPower = !RUNNING_TYPES.has(sportType?.toLowerCase())
const hasBests = lapBests && Object.keys(lapBests).length > 0
return (
<div className="overflow-x-auto">
<table className="w-full text-sm">
@@ -12,6 +13,8 @@ export default function LapTable({ laps, sportType }) {
<th className="text-left pb-2 font-medium">Lap</th>
<th className="text-right pb-2 font-medium">Distance</th>
<th className="text-right pb-2 font-medium">Time</th>
{hasBests && <th className="text-right pb-2 font-medium">Best</th>}
{hasBests && <th className="text-right pb-2 font-medium">Δ</th>}
<th className="text-right pb-2 font-medium">Pace</th>
<th className="text-right pb-2 font-medium">Avg HR</th>
<th className="text-right pb-2 font-medium">Cadence</th>
@@ -19,25 +22,40 @@ export default function LapTable({ laps, sportType }) {
</tr>
</thead>
<tbody>
{laps.map((lap) => (
<tr key={lap.lap_number} className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
<td className="py-2 text-gray-400">{lap.lap_number}</td>
<td className="py-2 text-right text-gray-200">{formatDistance(lap.distance_m)}</td>
<td className="py-2 text-right text-gray-200">{formatDuration(lap.duration_s)}</td>
<td className="py-2 text-right text-gray-200">{formatPace(lap.avg_speed_ms, sportType)}</td>
<td className="py-2 text-right">
<span className="text-red-400">{formatHeartRate(lap.avg_heart_rate)}</span>
</td>
<td className="py-2 text-right text-gray-400">
{lap.avg_cadence ? formatCadence(lap.avg_cadence, sportType) : '--'}
</td>
{showPower && (
<td className="py-2 text-right text-gray-400">
{lap.avg_power ? `${Math.round(lap.avg_power)} W` : '--'}
{laps.map((lap) => {
const best = hasBests ? lapBests[String(lap.lap_number)] : null
const delta = best != null && lap.duration_s != null ? lap.duration_s - best : null
const isBest = delta != null && delta <= 0.5
return (
<tr key={lap.lap_number} className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
<td className="py-2 text-gray-400">{lap.lap_number}</td>
<td className="py-2 text-right text-gray-200">{formatDistance(lap.distance_m)}</td>
<td className={`py-2 text-right ${isBest ? 'text-yellow-400 font-semibold' : 'text-gray-200'}`}>{formatDuration(lap.duration_s)}</td>
{hasBests && (
<td className="py-2 text-right font-mono text-gray-500">{best != null ? formatDuration(best) : '--'}</td>
)}
{hasBests && (
<td className={`py-2 text-right font-mono ${
delta == null ? 'text-gray-700' : isBest ? 'text-yellow-400' : delta < 0 ? 'text-green-400' : 'text-red-400'
}`}>
{delta == null ? '--' : isBest ? '🏆' : `${delta > 0 ? '+' : ''}${formatDuration(Math.abs(delta))}`}
</td>
)}
<td className="py-2 text-right text-gray-200">{formatPace(lap.avg_speed_ms, sportType)}</td>
<td className="py-2 text-right">
<span className="text-red-400">{formatHeartRate(lap.avg_heart_rate)}</span>
</td>
)}
</tr>
))}
<td className="py-2 text-right text-gray-400">
{lap.avg_cadence ? formatCadence(lap.avg_cadence, sportType) : '--'}
</td>
{showPower && (
<td className="py-2 text-right text-gray-400">
{lap.avg_power ? `${Math.round(lap.avg_power)} W` : '--'}
</td>
)}
</tr>
)
})}
</tbody>
</table>
</div>