Add trend-range gating, vehicle filter, sync cancel, moving time, and UI fixes
- Grey out trend ranges beyond available health history - Reject implausibly fast (vehicle) activities on upload with feedback - Add cancel button + cooperative cancellation for Garmin sync - Show daily steps prominently on the dashboard - Clear errors for malformed/empty upload ZIPs - Snap-target dot when drawing a segment on the map - Time-axis fallback for stationary/HIIT HR timelines; hide map when no GPS - Parse and display moving time (timer) vs elapsed; backfill task - Restyle SegmentsPanel like RouteLeaderboard; Laps/Routes/Segments on one row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
AreaChart, Area, BarChart, Bar, ReferenceLine, ReferenceArea,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell,
|
||||
} from 'recharts'
|
||||
import { format, subDays } from 'date-fns'
|
||||
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'
|
||||
@@ -885,6 +885,18 @@ export default function HealthPage() {
|
||||
[allDays],
|
||||
)
|
||||
|
||||
// Disable trend ranges that reach further back than the data goes. Keep every
|
||||
// range up to and including the first one that already covers the full history
|
||||
// enabled; ranges beyond that would only show the same (full) data. While the
|
||||
// history is still loading we leave all ranges enabled.
|
||||
const maxEnabledRangeIdx = useMemo(() => {
|
||||
if (!allDaysSorted.length) return RANGES.length - 1
|
||||
const oldest = allDaysSorted[allDaysSorted.length - 1].date
|
||||
const span = differenceInCalendarDays(new Date(), parseISO(oldest))
|
||||
const idx = RANGES.findIndex(r => r.days >= span)
|
||||
return idx === -1 ? RANGES.length - 1 : idx
|
||||
}, [allDaysSorted])
|
||||
|
||||
const selectedDay = useMemo(() => {
|
||||
if (!selectedDateStr) return allDaysSorted[0] || null
|
||||
return allDaysSorted.find(d => d.date === selectedDateStr) || null
|
||||
@@ -970,16 +982,24 @@ export default function HealthPage() {
|
||||
<p className="text-xs text-gray-600">Click any point to load that day above</p>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
{RANGES.map(({ label, days }) => (
|
||||
<button key={label} onClick={() => setRangeDays(days)}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
rangeDays === days
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'border-gray-700 text-gray-400 hover:text-white'
|
||||
}`}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
{RANGES.map(({ label, days }, i) => {
|
||||
const disabled = i > maxEnabledRangeIdx
|
||||
return (
|
||||
<button key={label}
|
||||
onClick={() => !disabled && setRangeDays(days)}
|
||||
disabled={disabled}
|
||||
title={disabled ? 'Not enough history for this range' : undefined}
|
||||
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
disabled
|
||||
? 'border-gray-800 text-gray-700 opacity-40 cursor-not-allowed'
|
||||
: rangeDays === days
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'border-gray-700 text-gray-400 hover:text-white'
|
||||
}`}>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user