Round 2: body battery redesign, profile cleanup, segment integration, route/segment records
Build and push images / validate (push) Successful in 18s
Build and push images / build-backend (push) Successful in 31s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Successful in 34s

- Body battery: replace circular ring with compact full-height colored bar chart,
  level as line overlay, legend shows only types present in data
- Dashboard: add mini body battery summary card above health today panel
- Profile: remove editable resting HR and manual weight log; show 7-day avg
  resting HR and latest Garmin weight as read-only
- Backend: add GET /routes/{id}/segment-bests bulk endpoint (fetches all matched
  activity data points in one query, computes best segment time per segment)
- Backend: add GET /records/routes for fastest activity per named route
- Routes page: add Segments panel to route detail (grouped as 1km splits vs
  hills/turns, best times, delete, theoretical best)
- Activity detail page: show segment times computed client-side from data points,
  🏆 badge if new best
- Records page: add Route Records and Segment Records tabs alongside Distance PRs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:14:00 +01:00
parent 02eccad578
commit 568dc31e97
8 changed files with 602 additions and 199 deletions
+60 -21
View File
@@ -9,6 +9,42 @@ import {
formatDate, sportIcon, formatSleep,
} from '../utils/format'
function bbLevelColor(level) {
if (level == null) return '#6b7280'
if (level >= 75) return '#3b82f6'
if (level >= 50) return '#22c55e'
if (level >= 25) return '#f59e0b'
return '#ef4444'
}
function MiniBodyBattery({ bb }) {
if (!bb?.end_level && !bb?.charged) return null
const { charged, drained, start_level, end_level } = bb
const color = bbLevelColor(end_level)
return (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-300">Body Battery</h3>
<Link to="/health" className="text-xs text-blue-400 hover:underline">View </Link>
</div>
<div className="flex items-baseline gap-3 flex-wrap">
{end_level != null && (
<span className="text-3xl font-bold" style={{ color }}>{Math.round(end_level)}</span>
)}
{charged != null && (
<span className="text-sm font-semibold text-green-400">+{charged}</span>
)}
{drained != null && (
<span className="text-sm font-semibold text-orange-400">-{drained}</span>
)}
</div>
{start_level != null && end_level != null && (
<p className="text-xs text-gray-500 mt-1">{start_level} {end_level} today</p>
)}
</div>
)
}
function WeeklyChart({ activities }) {
const navigate = useNavigate()
@@ -115,27 +151,30 @@ export default function DashboardPage() {
<WeeklyChart activities={allActivities} />
</div>
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4 space-y-3">
<h3 className="text-sm font-medium text-gray-300">Health today</h3>
{latest ? (
<>
{[
['HRV', latest.hrv_nightly_avg ? `${Math.round(latest.hrv_nightly_avg)} ms` : '--'],
['Sleep score', latest.sleep_score ? Math.round(latest.sleep_score) : '--'],
['Steps', latest.steps?.toLocaleString() ?? '--'],
['VO2 Max', latest.vo2max ? latest.vo2max.toFixed(1) : '--'],
['Stress', latest.avg_stress ? Math.round(latest.avg_stress) : '--'],
].map(([label, val]) => (
<div key={label} className="flex justify-between text-sm">
<span className="text-gray-500">{label}</span>
<span className="text-white">{val}</span>
</div>
))}
<Link to="/health" className="block text-xs text-blue-400 hover:underline mt-2">View full health dashboard </Link>
</>
) : (
<p className="text-xs text-gray-600">No health data. Import a Garmin export.</p>
)}
<div className="flex flex-col gap-4">
<MiniBodyBattery bb={latest?.body_battery} />
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4 space-y-3">
<h3 className="text-sm font-medium text-gray-300">Health today</h3>
{latest ? (
<>
{[
['HRV', latest.hrv_nightly_avg ? `${Math.round(latest.hrv_nightly_avg)} ms` : '--'],
['Sleep score', latest.sleep_score ? Math.round(latest.sleep_score) : '--'],
['Steps', latest.steps?.toLocaleString() ?? '--'],
['VO2 Max', latest.vo2max ? latest.vo2max.toFixed(1) : '--'],
['Stress', latest.avg_stress ? Math.round(latest.avg_stress) : '--'],
].map(([label, val]) => (
<div key={label} className="flex justify-between text-sm">
<span className="text-gray-500">{label}</span>
<span className="text-white">{val}</span>
</div>
))}
<Link to="/health" className="block text-xs text-blue-400 hover:underline mt-2">View full health dashboard </Link>
</>
) : (
<p className="text-xs text-gray-600">No health data. Import a Garmin export.</p>
)}
</div>
</div>
</div>