Segments and Av HR update
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 22s

This commit is contained in:
2026-06-07 17:12:27 +01:00
parent 4a4cbdcc92
commit bf1920eb9d
8 changed files with 299 additions and 98 deletions
+67 -11
View File
@@ -153,16 +153,71 @@ function BodyBatteryChart({ bb, hiresValues }) {
)
}
function SleepStagesBar({ deep, light, rem, awake }) {
const total = (deep || 0) + (light || 0) + (rem || 0) + (awake || 0)
if (!total) return null
const pct = s => `${((s || 0) / total * 100).toFixed(1)}%`
// Sleep timeline bar spanning from sleep_start to sleep_end with proportional stage coloring
function SleepTimeline({ sleepStart, sleepEnd, deep, light, rem, awake }) {
if (!sleepStart || !sleepEnd) return null
const stageSecs = (deep || 0) + (light || 0) + (rem || 0) + (awake || 0)
if (!stageSecs) return null
const startMs = new Date(sleepStart).getTime()
const endMs = new Date(sleepEnd).getTime()
const windowMs = endMs - startMs
if (windowMs <= 0) return null
// Build stage segments proportional to duration, but rendered across the sleep window
const stages = [
{ key: 'deep', secs: deep || 0, color: '#6366f1', label: 'Deep' },
{ key: 'rem', secs: rem || 0, color: '#8b5cf6', label: 'REM' },
{ key: 'light', secs: light || 0, color: '#a78bfa', label: 'Light' },
{ key: 'awake', secs: awake || 0, color: '#374151', label: 'Awake' },
].filter(s => s.secs > 0)
// Generate hour tick marks within the sleep window
const startHour = new Date(startMs)
startHour.setMinutes(0, 0, 0)
startHour.setHours(startHour.getHours() + 1)
const ticks = []
let tick = startHour.getTime()
while (tick < endMs) {
const pct = Math.min(100, Math.max(0, (tick - startMs) / windowMs * 100))
ticks.push({ pct, label: new Date(tick).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) })
tick += 3600000
}
return (
<div className="flex rounded-full overflow-hidden h-2.5 w-full">
<div style={{ width: pct(deep), backgroundColor: '#6366f1' }} />
<div style={{ width: pct(rem), backgroundColor: '#8b5cf6' }} />
<div style={{ width: pct(light), backgroundColor: '#a78bfa' }} />
<div style={{ width: pct(awake), backgroundColor: '#374151' }} />
<div className="space-y-1.5">
{/* Time bar */}
<div className="relative">
<div className="flex rounded-md overflow-hidden h-5 w-full">
{stages.map((s, i) => (
<div
key={s.key}
style={{ width: `${(s.secs / stageSecs * 100).toFixed(2)}%`, backgroundColor: s.color }}
/>
))}
</div>
{/* Tick marks */}
{ticks.map((t, i) => (
<div key={i} className="absolute top-0 h-5 flex flex-col items-center pointer-events-none" style={{ left: `${t.pct}%` }}>
<div className="w-px h-full bg-black/40" />
</div>
))}
</div>
{/* Time labels */}
<div className="relative h-4">
<span className="absolute left-0 text-xs text-gray-500" style={{ transform: 'translateX(-0%)' }}>
{new Date(startMs).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
</span>
{ticks.map((t, i) => (
<span key={i} className="absolute text-xs text-gray-600"
style={{ left: `${t.pct}%`, transform: 'translateX(-50%)' }}>
{t.label}
</span>
))}
<span className="absolute right-0 text-xs text-gray-500">
{new Date(endMs).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
)
}
@@ -253,11 +308,12 @@ function DailySnapshot({ day, avg30, intradayHr, bodyBattery, bbHires, onOlder,
</div>
{hasSleepStages ? (
<>
<SleepStagesBar
<SleepTimeline
sleepStart={day.sleep_start} sleepEnd={day.sleep_end}
deep={day.sleep_deep_s} light={day.sleep_light_s}
rem={day.sleep_rem_s} awake={day.sleep_awake_s}
/>
<div className="flex flex-wrap gap-x-5 gap-y-1.5">
<div className="flex flex-wrap gap-x-5 gap-y-1.5 mt-1">
{[
['Deep', day.sleep_deep_s, '#6366f1'],
['REM', day.sleep_rem_s, '#8b5cf6'],