HRV balanced dots, dashed gap lines, dashboard widgets + drag-to-edit layout
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 21s

- Green dots for balanced HRV (joining orange unbalanced / red low) on the trend
- Trend charts bridge data gaps with a dashed line instead of a blank break
- Dashboard: drop Health today; add VO2 max, small sleep, and HRV status widgets
- Dashboard: editable widget grid (react-grid-layout) with drag/resize, saved per-user

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:04:43 +01:00
parent af32a0bb7f
commit 8ed47d6042
7 changed files with 511 additions and 216 deletions
+10 -5
View File
@@ -1,7 +1,7 @@
import { useState, useMemo } from 'react'
import { useQuery, keepPreviousData } from '@tanstack/react-query'
import {
AreaChart, Area, BarChart, Bar, ReferenceLine, ReferenceArea,
AreaChart, Area, ComposedChart, Line, BarChart, Bar, ReferenceLine, ReferenceArea,
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell,
} from 'recharts'
import { format, subDays, differenceInCalendarDays, parseISO } from 'date-fns'
@@ -651,7 +651,7 @@ function DailySnapshot({ day, snapshotWeight, avg30, intradayHr, bodyBattery, bb
// Highlight problem days on a trend line by colouring the dot from a status field
// (e.g. HRV status): orange = unbalanced, red = low/poor. Other days get no dot.
const STATUS_DOT_COLORS = { unbalanced: '#f97316', low: '#ef4444', poor: '#ef4444' }
const STATUS_DOT_COLORS = { balanced: '#22c55e', unbalanced: '#f97316', low: '#ef4444', poor: '#ef4444' }
const statusDot = (statusKey) => (props) => {
const { cx, cy, payload } = props
const color = STATUS_DOT_COLORS[String(payload?.[statusKey] || '').toLowerCase()]
@@ -666,7 +666,7 @@ function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDa
)
return (
<ResponsiveContainer width="100%" height={height}>
<AreaChart
<ComposedChart
data={data}
margin={{ top: 4, right: 4, bottom: 4, left: 0 }}
style={{ cursor: onDayClick ? 'pointer' : 'default' }}
@@ -694,11 +694,15 @@ function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDa
{(referenceLines || []).map((rl, i) => (
<ReferenceLine key={i} {...rl} />
))}
{/* Dashed line bridging gaps (no data). Drawn first; the solid area below
covers it wherever real data exists, leaving only gaps shown dashed. */}
<Line type="monotone" dataKey={dataKey} stroke={color} strokeWidth={1.5}
strokeDasharray="4 4" dot={false} connectNulls isAnimationActive={false} legendType="none" />
<Area type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2}
fill={`url(#grad-${dataKey})`}
dot={statusDotKey ? statusDot(statusDotKey) : (showDots ? { fill: color, r: 3, strokeWidth: 0 } : false)}
connectNulls={connectNulls} isAnimationActive={false} />
</AreaChart>
connectNulls={false} isAnimationActive={false} />
</ComposedChart>
</ResponsiveContainer>
)
}
@@ -1030,6 +1034,7 @@ export default function HealthPage() {
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-300">HRV (nightly avg)</h3>
<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>
</div>