Health hypnogram, routes tiles, BB bar chart, segment delta
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s

- Sleep: store per-epoch stage timestamps in new sleep_stages JSON column;
  DailySnapshot now renders a proper 4-lane hypnogram (Awake/REM/Light/Deep)
  instead of the old proportional flat bar
- Body battery: replace grey background bars + white line with per-minute bars
  coloured by inferred type (sleep=indigo, rest=teal, active=orange, stable=grey)
  derived from sleep window + battery direction; Y-axis fixed 0-100
- Routes: convert sidebar list to tile grid sorted by most completions; tiles
  colour-bordered by sport type (blue=running, orange=cycling); completion count
  shown on each tile; detail panel displays below the grid when a tile is clicked
- Segments on activity detail: add column headers (This run / Best / Δ) and
  show signed time delta vs best, green when faster, red when slower

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 18:44:00 +01:00
parent 492418586a
commit 67fd4b3c96
8 changed files with 208 additions and 169 deletions
+20 -15
View File
@@ -222,28 +222,33 @@ export default function ActivityDetailPage() {
<h3 className="text-sm font-medium text-gray-300">Segments</h3>
<Link to="/segments" className="text-xs text-blue-400 hover:underline">Manage </Link>
</div>
<div className="space-y-1">
{/* Column headers */}
<div className="flex items-center gap-3 pb-1.5 border-b border-gray-800 mb-1">
<span className="flex-1 text-xs text-gray-600 uppercase tracking-wide">Segment</span>
<span className="font-mono text-xs w-14 text-right text-gray-600 uppercase tracking-wide">This run</span>
<span className="font-mono text-xs w-14 text-right text-gray-600 uppercase tracking-wide">Best</span>
<span className="font-mono text-xs w-14 text-right text-gray-600 uppercase tracking-wide">Δ</span>
</div>
<div className="space-y-0.5">
{segments.map(seg => {
const t = segmentTime(dataPoints, seg.start_distance_m, seg.end_distance_m)
const best = segmentBests?.find(b => b.segment_id === seg.id)
const isNewBest = t != null && best?.best_s != null && t <= best.best_s + 0.5
const delta = t != null && best?.best_s != null ? t - best.best_s : null
return (
<div key={seg.id} className="flex items-center gap-3 py-1.5 border-b border-gray-800/50 text-sm">
<div key={seg.id} className="flex items-center gap-3 py-1.5 border-b border-gray-800/40 text-sm">
<span className="flex-1 text-gray-300 text-xs truncate">{seg.name}</span>
<span className="font-mono text-xs w-14 text-right">
{t != null ? (
<span className={isNewBest ? 'text-yellow-400 font-semibold' : 'text-gray-200'}>
{formatDuration(t)}
</span>
) : (
<span className="text-gray-700">--</span>
)}
<span className={`font-mono text-xs w-14 text-right ${isNewBest ? 'text-yellow-400 font-semibold' : 'text-gray-200'}`}>
{t != null ? formatDuration(t) : <span className="text-gray-700">--</span>}
</span>
<span className="font-mono text-xs w-14 text-right text-gray-500">
{best?.best_s != null ? formatDuration(best.best_s) : '--'}
</span>
<span className={`font-mono text-xs w-14 text-right ${
isNewBest ? 'text-yellow-400' : delta == null ? 'text-gray-700' : delta <= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{isNewBest ? '🏆' : delta == null ? '--' : `${delta > 0 ? '+' : ''}${formatDuration(Math.abs(delta))}`}
</span>
{isNewBest && <span className="text-xs" title="New best">🏆</span>}
{!isNewBest && best?.best_s != null && (
<span className="text-gray-600 text-xs w-14 text-right">/{formatDuration(best.best_s)}</span>
)}
{!isNewBest && !best?.best_s && <span className="w-14" />}
</div>
)
})}