feat: sleep duration excludes awake time (actual sleep only)
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:
parent
b22481d4ce
commit
9aeb0de936
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user