HRV baseline band from Garmin + dashboard HRV colours + route name on recent activities + sync-now race fix
Build and push images / validate (push) Successful in 7s
Build and push images / build-backend (push) Successful in 1m9s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s

This commit is contained in:
2026-06-16 00:13:16 +01:00
parent e7123ee5db
commit d01f66223b
10 changed files with 98 additions and 45 deletions
+19 -20
View File
@@ -8,6 +8,7 @@ import { format, subDays, differenceInCalendarDays, parseISO } from 'date-fns'
import api from '../utils/api'
import { formatSleep, sportIcon } from '../utils/format'
import { BB_INFERRED_COLOR, BB_INFERRED_LABEL, bbLevelColor, inferBBType } from '../utils/bodyBattery'
import HrvBadge from '../components/ui/HrvBadge'
const RANGES = [
{ label: '1W', days: 7 },
@@ -414,18 +415,6 @@ function SleepStageFallbackBar({ deepS, remS, lightS, awakeS }) {
)
}
function HrvBadge({ status }) {
if (!status) return null
const palette = {
balanced: 'text-green-400 bg-green-400/10 border-green-400/30',
unbalanced: 'text-yellow-400 bg-yellow-400/10 border-yellow-400/30',
low: 'text-orange-400 bg-orange-400/10 border-orange-400/30',
poor: 'text-red-400 bg-red-400/10 border-red-400/30',
}
const cls = palette[status.toLowerCase()] || 'text-gray-400 bg-gray-400/10 border-gray-400/30'
return <span className={`text-xs px-2 py-0.5 rounded-full border ${cls}`}>{status}</span>
}
function NavArrow({ onClick, disabled, children }) {
return (
<button
@@ -688,15 +677,24 @@ const statusDot = (statusKey) => (props) => {
return <circle cx={cx} cy={cy} r={3.5} fill={color} stroke="#111827" strokeWidth={1} />
}
function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDate, onDayClick, connectNulls = false, showDots = false, domain, referenceLines, statusDotKey }) {
function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDate, onDayClick, connectNulls = false, showDots = false, domain, referenceLines, statusDotKey, bandLowKey, bandHighKey, bandColor = '#9ca3af' }) {
const vals = data.filter(d => d[dataKey] != null)
if (!vals.length) return (
<div className="flex items-center justify-center text-gray-600 text-xs" style={{ height }}>No data</div>
)
// Range band (e.g. Garmin's HRV baseline): Recharts renders an Area as a band
// when its dataKey resolves to a [low, high] pair.
const hasBand = bandLowKey && bandHighKey
const chartData = hasBand
? data.map(d => ({
...d,
__band: (d[bandLowKey] != null && d[bandHighKey] != null) ? [d[bandLowKey], d[bandHighKey]] : null,
}))
: data
return (
<ResponsiveContainer width="100%" height={height}>
<ComposedChart
data={data}
data={chartData}
margin={{ top: 4, right: 4, bottom: 4, left: 0 }}
style={{ cursor: onDayClick ? 'pointer' : 'default' }}
onClick={evt => {
@@ -716,7 +714,11 @@ function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDa
<YAxis tick={{ fontSize: 10, fill: '#6b7280' }} axisLine={false} tickLine={false} width={36}
tickFormatter={formatter} domain={domain} />
<Tooltip contentStyle={tooltipStyle} labelFormatter={d => format(new Date(d), 'MMM d, yyyy')}
formatter={v => [formatter ? formatter(v) : v?.toFixed(1)]} />
formatter={(v, name) => name === '__band' ? null : [formatter ? formatter(v) : v?.toFixed(1)]} />
{hasBand && (
<Area type="monotone" dataKey="__band" stroke="none" fill={bandColor} fillOpacity={0.18}
connectNulls isAnimationActive={false} legendType="none" activeDot={false} />
)}
{selectedDate && (
<ReferenceLine x={selectedDate} stroke="#60a5fa" strokeWidth={1.5} strokeDasharray="4 2" />
)}
@@ -1065,17 +1067,14 @@ export default function HealthPage() {
<div className="flex items-center gap-3 text-xs text-gray-500">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} /> Balanced</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#f97316' }} /> Unbalanced</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#ef4444' }} /> Low</span>
<span className="flex items-center gap-1"><span className="w-3 h-2 rounded-sm" style={{ background: '#9ca3af', opacity: 0.5 }} /> Baseline</span>
</div>
</div>
<MetricChart data={metrics} dataKey="hrv_nightly_avg" color="#8b5cf6"
formatter={v => `${Math.round(v)} ms`}
selectedDate={selDateForCharts} onDayClick={handleDayClick}
statusDotKey="hrv_status"
referenceLines={[
{ y: 20, stroke: '#f59e0b', strokeDasharray: '3 3', label: { value: 'Low', position: 'insideTopRight', fill: '#f59e0b', fontSize: 9 } },
{ y: 60, stroke: '#22c55e', strokeDasharray: '3 3', label: { value: 'Good', position: 'insideTopRight', fill: '#22c55e', fontSize: 9 } },
]}
bandLowKey="hrv_baseline_low" bandHighKey="hrv_baseline_upper" bandColor="#9ca3af"
/>
</div>