// Today tab — wired to /api/today + the last-14d slice of /api/sleep-trends. const { fmtBedtime, fmtDelta, fmtHoursMinutes } = window.HC_DATA; function Card({ children, style = {}, theme }) { const p = theme.palette; return (
{children}
); } function MetricMini({ label, value, unit, color, delta, deltaGood, sparkSeries, target, targetLabel, theme }) { const p = theme.palette; const deltaColor = deltaGood == null ? p.text3 : (deltaGood ? p.positive : p.warning); return (
{label}
{value}{unit}
{delta}
); } function DetailItem({ label, main, sub, stale, theme }) { const p = theme.palette; return (
{label}
{main}
{sub}
); } function ThisWeekRow({ label, filled, total, theme }) { const p = theme.palette; return (
{label}
{Array.from({ length: total }).map((_, i) => (
))}
); } function daysAgoStr(iso) { if (!iso) return '—'; const today = new Date(); today.setHours(0,0,0,0); const d = new Date(iso + 'T00:00:00'); const diff = Math.round((today - d) / 86400000); if (diff <= 0) return 'today'; if (diff === 1) return '1d ago'; return `${diff}d ago`; } function fmtShortDate(iso) { if (!iso) return ''; const d = new Date(iso + 'T00:00:00'); return `${['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()]} ${d.getDate()}`; } function TodayTab({ today }) { const theme = window.HC_THEME.useHCTheme(); const p = theme.palette; const d = theme.density; const sleep = today.sleep || {}; const training = today.training || {}; const readiness = today.readiness || {}; const sleepH = sleep.duration_h; const avg10 = today.sleep_10d_avg; const delta = (sleepH != null && avg10 != null) ? (sleepH - avg10) : null; const deltaPositive = delta != null && delta >= 0; const deltaStr = delta != null ? `${delta >= 0 ? '↑' : '↓'} ${Math.abs(delta).toFixed(1)}h vs 10d avg` : ''; const hrv = sleep.hrv; const rhr = sleep.lowest_hr; const readScore = readiness.score; const bedtime = sleep.bedtime; const efficiency = sleep.efficiency; // Sessions/wk (this week, Mon-Sun count via days_since_training only gives // gap, not weekly count). Use training summary: days_since_training is real. const daysSince = training.days_since_training; const isStale = daysSince != null && daysSince > 7; // Strength/run "this week" boxes — best-effort estimate from days_since. // The Trends data array would give the exact number, but we don't want a // second fetch on the Today path. Render based on days_since heuristic. // (When TrendsData is loaded after first viewing trends, the page does // refresh from /api/today, so this stays a snapshot.) return (
{/* Sleep card */}
{fmtHoursMinutes(sleepH)}
{deltaStr}
{today.sleep_score ?? '—'}
sleep score
{/* Metrics grid 2×2 */}
= today.readiness_10d_avg : null} sparkSeries="ready" />
{/* Drinks card — yesterday's intake, contextualizes last night's sleep */} {(() => { const yd = today.drinks_yesterday || {}; const ydUnits = yd.units || 0; const history14 = (today.drinks_14d || []).map((d) => d.units); const valueColor = ydUnits >= 3 ? p.warning : ydUnits > 0 ? p.text : p.text3; return (
Drinks · yesterday
14d
{ydUnits > 0 ? ( <>{ydUnits}u ) : ( )}
{yd.text && (
{yd.text}
)}
); })()} {/* Training card */}
Training
{daysSince != null ? daysSince : '—'} days since last
{training.comeback_mode && (
comeback mode · re-entry when ready
)}
{training.vo2max != null ? Math.round(training.vo2max) : '—'}
vo₂max
10 : false} /> 7 : false} /> 7 : false} />
{/* Context strip */}
{efficiency != null ? `Eff ${Math.round(efficiency)}%` : ''} {efficiency != null && today.resilience ? ' · ' : ''} {today.resilience ? today.resilience.charAt(0).toUpperCase() + today.resilience.slice(1) : ''} {(efficiency != null || today.resilience) && daysSince != null ? ' · ' : ''} {daysSince != null ? `Last training ${daysSince}d ago` : ''}
); } window.TodayTab = TodayTab; window.HCCard = Card;