From 9aeb0de9362a6cfdbf0c5a66a1408cfe23c5c8e8 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 22 Mar 2026 14:01:47 +0100 Subject: [PATCH] feat: sleep duration excludes awake time (actual sleep only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conceptual change: duration_minutes = actual sleep time (not time in bed) Backend: - Plausibility check: deep + rem + light = duration (awake separate) - Import: duration = deep + rem + light (without awake) - Updated error message: clarifies awake not counted Frontend: - Label: "Schlafdauer (reine Schlafzeit, Minuten)" - Auto-calculate: bedtime-waketime minus awake_minutes - Plausibility check: only validates sleep phases (not awake) - Both NewEntry and Edit mode updated Rationale: - Standard in sleep tracking (Apple Health shows "Sleep", not "Time in Bed") - Clearer semantics: duration = how long you slept - awake_minutes tracked separately for analysis - More intuitive for users Example: - Time in bed: 22:00 - 06:00 = 480 min (8h) - Awake phases: 30 min - Sleep duration: 450 min (7h 30min) ✓ Co-Authored-By: Claude Opus 4.6 --- backend/routers/sleep.py | 28 ++++++++++--------- frontend/src/pages/SleepPage.jsx | 48 +++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/backend/routers/sleep.py b/backend/routers/sleep.py index e346a94..dfdf8e6 100644 --- a/backend/routers/sleep.py +++ b/backend/routers/sleep.py @@ -171,14 +171,15 @@ def create_sleep( bedtime = data.bedtime if data.bedtime else None wake_time = data.wake_time if data.wake_time else None - # Plausibility check: phases should sum to duration (tolerance: 5 min) - if any([data.deep_minutes, data.rem_minutes, data.light_minutes, data.awake_minutes]): - phase_sum = (data.deep_minutes or 0) + (data.rem_minutes or 0) + (data.light_minutes or 0) + (data.awake_minutes or 0) - diff = abs(data.duration_minutes - phase_sum) + # Plausibility check: sleep phases (deep+rem+light) should sum to duration + # Note: awake_minutes is NOT part of sleep duration (tracked separately) + if any([data.deep_minutes, data.rem_minutes, data.light_minutes]): + sleep_phase_sum = (data.deep_minutes or 0) + (data.rem_minutes or 0) + (data.light_minutes or 0) + diff = abs(data.duration_minutes - sleep_phase_sum) if diff > 5: raise HTTPException( 400, - f"Plausibilitätsprüfung fehlgeschlagen: Phasen-Summe ({phase_sum} min) weicht um {diff} min von Gesamtdauer ({data.duration_minutes} min) ab. Max. Toleranz: 5 min." + f"Plausibilitätsprüfung fehlgeschlagen: Schlafphasen-Summe ({sleep_phase_sum} min) weicht um {diff} min von Schlafdauer ({data.duration_minutes} min) ab. Max. Toleranz: 5 min. Hinweis: Wachphasen werden nicht zur Schlafdauer gezählt." ) with get_db() as conn: @@ -231,14 +232,15 @@ def update_sleep( bedtime = data.bedtime if data.bedtime else None wake_time = data.wake_time if data.wake_time else None - # Plausibility check: phases should sum to duration (tolerance: 5 min) - if any([data.deep_minutes, data.rem_minutes, data.light_minutes, data.awake_minutes]): - phase_sum = (data.deep_minutes or 0) + (data.rem_minutes or 0) + (data.light_minutes or 0) + (data.awake_minutes or 0) - diff = abs(data.duration_minutes - phase_sum) + # Plausibility check: sleep phases (deep+rem+light) should sum to duration + # Note: awake_minutes is NOT part of sleep duration (tracked separately) + if any([data.deep_minutes, data.rem_minutes, data.light_minutes]): + sleep_phase_sum = (data.deep_minutes or 0) + (data.rem_minutes or 0) + (data.light_minutes or 0) + diff = abs(data.duration_minutes - sleep_phase_sum) if diff > 5: raise HTTPException( 400, - f"Plausibilitätsprüfung fehlgeschlagen: Phasen-Summe ({phase_sum} min) weicht um {diff} min von Gesamtdauer ({data.duration_minutes} min) ab. Max. Toleranz: 5 min." + f"Plausibilitätsprüfung fehlgeschlagen: Schlafphasen-Summe ({sleep_phase_sum} min) weicht um {diff} min von Schlafdauer ({data.duration_minutes} min) ab. Max. Toleranz: 5 min. Hinweis: Wachphasen werden nicht zur Schlafdauer gezählt." ) with get_db() as conn: @@ -557,12 +559,12 @@ async def import_apple_health_sleep( cur = get_cursor(conn) for date, night in nights_dict.items(): - # Calculate total duration (sum of all phases) + # Calculate sleep duration (deep + rem + light, WITHOUT awake) + # Note: awake_minutes tracked separately, not part of sleep duration duration_minutes = ( night['deep_minutes'] + night['rem_minutes'] + - night['light_minutes'] + - night['awake_minutes'] + night['light_minutes'] ) # Calculate wake_count (number of awake segments) diff --git a/frontend/src/pages/SleepPage.jsx b/frontend/src/pages/SleepPage.jsx index 49dfa14..1237880 100644 --- a/frontend/src/pages/SleepPage.jsx +++ b/frontend/src/pages/SleepPage.jsx @@ -336,7 +336,7 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel const [plausibilityError, setPlausibilityError] = useState(null) const [suggestedDuration, setSuggestedDuration] = useState(null) - // Auto-calculate duration from bedtime + wake_time + // Auto-calculate duration from bedtime + wake_time (minus awake time) useEffect(() => { if (editing && formData.bedtime && formData.wake_time) { const [bedH, bedM] = formData.bedtime.split(':').map(Number) @@ -349,22 +349,29 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel wakeMinutes += 24 * 60 } - const duration = wakeMinutes - bedMinutes + let duration = wakeMinutes - bedMinutes + + // Subtract awake time to get actual sleep duration + const awake = parseInt(formData.awake_minutes) || 0 + if (awake > 0) { + duration = Math.max(0, duration - awake) + } + setSuggestedDuration(duration) } else { setSuggestedDuration(null) } - }, [editing, formData.bedtime, formData.wake_time]) + }, [editing, formData.bedtime, formData.wake_time, formData.awake_minutes]) - // Live plausibility check + // Live plausibility check (sleep phases only, awake not counted) useEffect(() => { if (!editing) return - const phases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes, formData.awake_minutes] - if (phases.some(p => p !== '' && p !== null)) { - const sum = phases.reduce((a, b) => a + (parseInt(b) || 0), 0) + const sleepPhases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes] + if (sleepPhases.some(p => p !== '' && p !== null)) { + const sum = sleepPhases.reduce((a, b) => a + (parseInt(b) || 0), 0) const diff = Math.abs(formData.duration_minutes - sum) if (diff > 5) { - setPlausibilityError(`Phasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min)`) + setPlausibilityError(`Schlafphasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min). Wachphasen nicht mitgezählt.`) } else { setPlausibilityError(null) } @@ -408,7 +415,7 @@ function SleepEntry({ entry, expanded, editing, onToggleExpand, onEdit, onCancel
-
Schlafdauer (Minuten)
+
Schlafdauer (reine Schlafzeit, Minuten)
{ if (formData.bedtime && formData.wake_time) { const [bedH, bedM] = formData.bedtime.split(':').map(Number) @@ -745,21 +752,28 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) { wakeMinutes += 24 * 60 } - const duration = wakeMinutes - bedMinutes + let duration = wakeMinutes - bedMinutes + + // Subtract awake time to get actual sleep duration + const awake = parseInt(formData.awake_minutes) || 0 + if (awake > 0) { + duration = Math.max(0, duration - awake) + } + setSuggestedDuration(duration) } else { setSuggestedDuration(null) } - }, [formData.bedtime, formData.wake_time]) + }, [formData.bedtime, formData.wake_time, formData.awake_minutes]) - // Live plausibility check + // Live plausibility check (sleep phases only, awake not counted) useEffect(() => { - const phases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes, formData.awake_minutes] - if (phases.some(p => p !== '' && p !== null)) { - const sum = phases.reduce((a, b) => a + (parseInt(b) || 0), 0) + const sleepPhases = [formData.deep_minutes, formData.rem_minutes, formData.light_minutes] + if (sleepPhases.some(p => p !== '' && p !== null)) { + const sum = sleepPhases.reduce((a, b) => a + (parseInt(b) || 0), 0) const diff = Math.abs(formData.duration_minutes - sum) if (diff > 5) { - setPlausibilityError(`Phasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min)`) + setPlausibilityError(`Schlafphasen-Summe (${sum} min) weicht um ${diff} min ab (Toleranz: 5 min). Wachphasen nicht mitgezählt.`) } else { setPlausibilityError(null) }