Health hypnogram, routes tiles, BB bar chart, segment delta
- 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:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user