feat: sleep duration excludes awake time (actual sleep only)
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

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 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-22 14:01:47 +01:00
parent b22481d4ce
commit 9aeb0de936
2 changed files with 46 additions and 30 deletions

View File

@ -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)

View File

@ -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
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 10 }}>
<div>
<div className="form-label">Schlafdauer (Minuten)</div>
<div className="form-label">Schlafdauer (reine Schlafzeit, Minuten)</div>
<input
type="number"
className="form-input"
@ -731,7 +738,7 @@ function NewEntryForm({ onSave, onCancel, formatDuration }) {
const [showDetail, setShowDetail] = useState(false)
const [suggestedDuration, setSuggestedDuration] = useState(null)
// Auto-calculate duration from bedtime + wake_time
// Auto-calculate duration from bedtime + wake_time (minus awake time)
useEffect(() => {
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)
}