Add sleep scoring system and refactor health sleep fields

Introduces a comprehensive sleep scoring module with detailed calculation logic and integration into health sleep creation and editing endpoints. Refactors HealthSleep model and schema to use integer types for heart rate, SpO2, respiration, and sleep stress fields. Updates related tests and documentation to reflect the new scoring system and data type changes.
This commit is contained in:
João Vitória Silva
2025-12-07 15:21:25 +00:00
parent 9bdbefca90
commit 7d62434da2
18 changed files with 1640 additions and 182 deletions

View File

@@ -398,7 +398,7 @@ def fetch_and_process_sleep_by_dates(
in health_sleep_schema.SleepScore._value2member_map_
else None
),
avg_sleep_stress=sleep_dto.get("avgSleepStress"),
avg_sleep_stress=int(sleep_dto.get("avgSleepStress")),
sleep_stress_score=(
health_sleep_schema.SleepScore(sleep_stress_score.get("qualifierKey"))
if sleep_stress_score

View File

@@ -157,7 +157,7 @@ def get_health_sleep_by_date(
return (
db.query(health_sleep_models.HealthSleep)
.filter(
health_sleep_models.HealthSleep.date == date,
health_sleep_models.HealthSleep.date == func.date(date),
health_sleep_models.HealthSleep.user_id == user_id,
)
.first()

View File

@@ -35,16 +35,16 @@ class HealthSleep(Base):
light_sleep_seconds (int): Duration of light sleep in seconds.
rem_sleep_seconds (int): Duration of REM sleep in seconds.
awake_sleep_seconds (int): Duration of awake time in seconds.
avg_heart_rate (Decimal): Average heart rate during sleep.
avg_heart_rate (int): Average heart rate during sleep.
min_heart_rate (int): Minimum heart rate during sleep.
max_heart_rate (int): Maximum heart rate during sleep.
avg_spo2 (Decimal): Average SpO2 oxygen saturation percentage.
avg_spo2 (int): Average SpO2 oxygen saturation percentage.
lowest_spo2 (int): Lowest SpO2 reading during sleep.
highest_spo2 (int): Highest SpO2 reading during sleep.
avg_respiration (Decimal): Average respiration rate.
lowest_respiration (Decimal): Lowest respiration rate.
highest_respiration (Decimal): Highest respiration rate.
avg_stress_level (Decimal): Average stress level during sleep.
avg_respiration (int): Average respiration rate.
lowest_respiration (int): Lowest respiration rate.
highest_respiration (int): Highest respiration rate.
avg_stress_level (int): Average stress level during sleep.
awake_count (int): Number of times awakened during sleep.
restless_moments_count (int): Count of restless moments.
sleep_score_overall (int): Overall sleep score (0-100).
@@ -60,7 +60,7 @@ class HealthSleep(Base):
rem_percentage_score (str): REM sleep percentage score.
deep_percentage_score (str): Deep sleep percentage score.
light_percentage_score (str): Light sleep percentage score.
avg_sleep_stress (Decimal): Average sleep stress level.
avg_sleep_stress (int): Average sleep stress level.
sleep_stress_score (str): Sleep stress score.
user (relationship): SQLAlchemy relationship to the User model.
"""
@@ -262,7 +262,7 @@ class HealthSleep(Base):
comment="Light sleep percentage score",
)
avg_sleep_stress = Column(
DECIMAL(precision=10, scale=2),
Integer,
nullable=True,
comment="Average sleep stress level",
)

View File

@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
import health_sleep.schema as health_sleep_schema
import health_sleep.crud as health_sleep_crud
import health_sleep.sleep_scoring as sleep_scoring
import auth.security as auth_security
@@ -152,6 +153,9 @@ async def create_health_sleep(
if not health_sleep.date:
raise HTTPException(status_code=400, detail="Date field is required.")
# Calculate sleep scores before saving
sleep_scoring._calculate_and_set_sleep_scores(health_sleep)
# Convert date to string format for CRUD function
date_str = health_sleep.date.isoformat()
@@ -207,6 +211,9 @@ async def edit_health_sleep(
HTTPException: May raise various HTTP exceptions if authorization fails, user is not
found, or database operations fail.
"""
# Recalculate sleep scores when editing
sleep_scoring._calculate_and_set_sleep_scores(health_sleep)
# Updates the health_sleep in the database and returns it
return health_sleep_crud.edit_health_sleep(token_user_id, health_sleep, db)

View File

@@ -176,7 +176,7 @@ class HealthSleep(BaseModel):
rem_percentage_score: SleepScore | None = None
deep_percentage_score: SleepScore | None = None
light_percentage_score: SleepScore | None = None
avg_sleep_stress: Decimal | None = None
avg_sleep_stress: int | None = None
sleep_stress_score: SleepScore | None = None
model_config = ConfigDict(
@@ -188,11 +188,10 @@ class HealthSleep(BaseModel):
@field_validator("avg_heart_rate", "min_heart_rate", "max_heart_rate")
@classmethod
def validate_heart_rate(cls, v: int | Decimal | None) -> int | Decimal | None:
def validate_heart_rate(cls, v: int | None) -> int | None:
"""Validate heart rate is within reasonable range (20-220 bpm)."""
if v is not None:
value = float(v) if isinstance(v, Decimal) else v
if value < 20 or value > 220:
if v < 20 or v > 220:
raise ValueError("Heart rate must be between 20 and 220 bpm")
return v
@@ -207,11 +206,10 @@ class HealthSleep(BaseModel):
@field_validator("avg_spo2", "lowest_spo2", "highest_spo2")
@classmethod
def validate_spo2(cls, v: int | Decimal | None) -> int | Decimal | None:
def validate_spo2(cls, v: int | None) -> int | None:
"""Validate SpO2 is within reasonable range (70-100%)."""
if v is not None:
value = float(v) if isinstance(v, Decimal) else v
if value < 70 or value > 100:
if v < 70 or v > 100:
raise ValueError("SpO2 must be between 70 and 100%")
return v

View File

@@ -0,0 +1,547 @@
"""
Sleep scoring calculations.
"""
import health_sleep.schema as health_sleep_schema
from datetime import datetime
def _calculate_sleep_duration_hours(
total_sleep_seconds: int | None,
) -> float:
"""
Convert total sleep seconds to hours.
Args:
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Sleep duration in hours as float.
"""
if total_sleep_seconds is None:
return 0.0
return total_sleep_seconds / 3600.0
def _calculate_stage_percentage(
stage_seconds: int | None,
total_sleep_seconds: int | None,
) -> float:
"""
Calculate percentage of a sleep stage relative to total sleep.
Args:
stage_seconds: Duration of specific sleep stage in seconds.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Percentage of sleep stage (0-100).
"""
if stage_seconds is None or total_sleep_seconds is None or total_sleep_seconds == 0:
return 0.0
return (stage_seconds / total_sleep_seconds) * 100.0
def calculate_sleep_score_duration(
sleep_start_time: datetime | None,
sleep_end_time: datetime | None,
total_sleep_seconds: int | None,
) -> tuple[int, str]:
"""
Calculate sleep duration score based on total sleep time.
Scoring criteria (based on adult sleep recommendations):
- 7-9 hours: EXCELLENT (90-100 points)
- 6-7 or 9-10 hours: GOOD (70-89 points)
- 5-6 or 10-11 hours: FAIR (50-69 points)
- < 5 or > 11 hours: POOR (0-49 points)
Args:
sleep_start_time: Start time of sleep session.
sleep_end_time: End time of sleep session.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Tuple of (score points 0-100, score label).
"""
if total_sleep_seconds is None or total_sleep_seconds <= 0:
return (0, "POOR")
hours = _calculate_sleep_duration_hours(total_sleep_seconds)
# Optimal sleep range: 7-9 hours
if 7.0 <= hours <= 9.0:
# Scale within optimal range (7h = 90pts, 8h = 100pts, 9h
# = 90pts)
if hours <= 8.0:
score = int(90 + ((hours - 7.0) * 10))
else:
score = int(100 - ((hours - 8.0) * 10))
return (score, "EXCELLENT")
# Good sleep range: 6-7 or 9-10 hours
if 6.0 <= hours < 7.0:
score = int(70 + ((hours - 6.0) * 20))
return (score, "GOOD")
if 9.0 < hours <= 10.0:
score = int(89 - ((hours - 9.0) * 19))
return (score, "GOOD")
# Fair sleep range: 5-6 or 10-11 hours
if 5.0 <= hours < 6.0:
score = int(50 + ((hours - 5.0) * 20))
return (score, "FAIR")
if 10.0 < hours <= 11.0:
score = int(69 - ((hours - 10.0) * 19))
return (score, "FAIR")
# Poor sleep: < 5 or > 11 hours
if hours < 5.0:
score = max(0, int(49 * (hours / 5.0)))
return (score, "POOR")
# > 11 hours
score = max(0, int(49 - ((hours - 11.0) * 5)))
return (score, "POOR")
def calculate_sleep_score_quality(
deep_sleep_seconds: int | None,
light_sleep_seconds: int | None,
rem_sleep_seconds: int | None,
awake_sleep_seconds: int | None,
total_sleep_seconds: int | None,
) -> tuple[int, str]:
"""
Calculate sleep quality score based on sleep stage composition.
Scoring based on optimal sleep stage percentages:
- Deep: 13-23% optimal (peak at 18%)
- REM: 20-25% optimal (peak at 22.5%)
- Light: 45-55% optimal (peak at 50%)
- Awake: < 5% optimal
Args:
deep_sleep_seconds: Duration of deep sleep in seconds.
light_sleep_seconds: Duration of light sleep in seconds.
rem_sleep_seconds: Duration of REM sleep in seconds.
awake_sleep_seconds: Duration of awake time in seconds.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Tuple of (score points 0-100, score label).
"""
if total_sleep_seconds is None or total_sleep_seconds <= 0:
return (0, "POOR")
# Calculate percentages
deep_pct = _calculate_stage_percentage(deep_sleep_seconds, total_sleep_seconds)
rem_pct = _calculate_stage_percentage(rem_sleep_seconds, total_sleep_seconds)
light_pct = _calculate_stage_percentage(light_sleep_seconds, total_sleep_seconds)
awake_pct = _calculate_stage_percentage(awake_sleep_seconds, total_sleep_seconds)
# Score each component (0-100)
deep_score = _score_sleep_stage(deep_pct, 13.0, 23.0, 18.0)
rem_score = _score_sleep_stage(rem_pct, 20.0, 25.0, 22.5)
light_score = _score_sleep_stage(light_pct, 45.0, 55.0, 50.0)
# Awake penalty (lower is better)
if awake_pct <= 5.0:
awake_score = 100 - (awake_pct * 4)
else:
awake_score = max(0, 80 - ((awake_pct - 5.0) * 10))
# Weighted average: deep 25%, REM 30%, light 25%, awake 20%
total_score = int(
(deep_score * 0.25)
+ (rem_score * 0.30)
+ (light_score * 0.25)
+ (awake_score * 0.20)
)
# Determine label
if total_score >= 90:
return (total_score, "EXCELLENT")
if total_score >= 70:
return (total_score, "GOOD")
if total_score >= 50:
return (total_score, "FAIR")
return (total_score, "POOR")
def _score_sleep_stage(
actual_pct: float,
min_optimal: float,
max_optimal: float,
peak_optimal: float,
) -> float:
"""
Score a sleep stage percentage against optimal range.
Args:
actual_pct: Actual percentage of sleep stage.
min_optimal: Minimum optimal percentage.
max_optimal: Maximum optimal percentage.
peak_optimal: Peak optimal percentage.
Returns:
Score from 0-100.
"""
if min_optimal <= actual_pct <= max_optimal:
# Within optimal range, calculate distance from peak
if actual_pct <= peak_optimal:
# Between min and peak
range_span = peak_optimal - min_optimal
if range_span > 0:
score = 80 + ((actual_pct - min_optimal) / range_span) * 20
else:
score = 100
else:
# Between peak and max
range_span = max_optimal - peak_optimal
if range_span > 0:
score = 100 - ((actual_pct - peak_optimal) / range_span) * 20
else:
score = 100
return min(100, max(80, score))
if actual_pct < min_optimal:
# Below optimal range
if min_optimal > 0:
score = 80 * (actual_pct / min_optimal)
else:
score = 0
return max(0, score)
# Above optimal range
over_pct = actual_pct - max_optimal
score = max(0, 80 - (over_pct * 4))
return score
def calculate_awake_count_score(
awake_count: int | None,
) -> tuple[int, str]:
"""
Calculate score based on number of awakenings during sleep.
Scoring criteria:
- 0-1 awakenings: EXCELLENT (90-100 points)
- 2-3 awakenings: GOOD (70-89 points)
- 4-5 awakenings: FAIR (50-69 points)
- 6+ awakenings: POOR (0-49 points)
Args:
awake_count: Number of times awakened during sleep.
Returns:
Tuple of (score points 0-100, score label).
"""
if awake_count is None:
return (50, "FAIR")
if awake_count <= 1:
score = 100 - (awake_count * 5)
return (score, "EXCELLENT")
if awake_count <= 3:
score = 89 - ((awake_count - 2) * 10)
return (score, "GOOD")
if awake_count <= 5:
score = 69 - ((awake_count - 4) * 10)
return (score, "FAIR")
# 6+ awakenings
score = max(0, 49 - ((awake_count - 6) * 5))
return (score, "POOR")
def calculate_rem_percentage_score(
rem_sleep_seconds: int | None,
total_sleep_seconds: int | None,
) -> tuple[int, str]:
"""
Calculate score for REM sleep percentage.
Optimal REM: 20-25% of total sleep (peak at 22.5%).
Args:
rem_sleep_seconds: Duration of REM sleep in seconds.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Tuple of (score points 0-100, score label).
"""
if total_sleep_seconds is None or total_sleep_seconds <= 0:
return (0, "POOR")
rem_pct = _calculate_stage_percentage(rem_sleep_seconds, total_sleep_seconds)
score = int(_score_sleep_stage(rem_pct, 20.0, 25.0, 22.5))
if score >= 90:
return (score, "EXCELLENT")
if score >= 70:
return (score, "GOOD")
if score >= 50:
return (score, "FAIR")
return (score, "POOR")
def calculate_deep_percentage_score(
deep_sleep_seconds: int | None,
total_sleep_seconds: int | None,
) -> tuple[int, str]:
"""
Calculate score for deep sleep percentage.
Optimal deep sleep: 13-23% of total sleep (peak at 18%).
Args:
deep_sleep_seconds: Duration of deep sleep in seconds.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Tuple of (score points 0-100, score label).
"""
if total_sleep_seconds is None or total_sleep_seconds <= 0:
return (0, "POOR")
deep_pct = _calculate_stage_percentage(deep_sleep_seconds, total_sleep_seconds)
score = int(_score_sleep_stage(deep_pct, 13.0, 23.0, 18.0))
if score >= 90:
return (score, "EXCELLENT")
if score >= 70:
return (score, "GOOD")
if score >= 50:
return (score, "FAIR")
return (score, "POOR")
def calculate_light_percentage_score(
light_sleep_seconds: int | None,
total_sleep_seconds: int | None,
) -> tuple[int, str]:
"""
Calculate score for light sleep percentage.
Optimal light sleep: 45-55% of total sleep (peak at 50%).
Args:
light_sleep_seconds: Duration of light sleep in seconds.
total_sleep_seconds: Total sleep duration in seconds.
Returns:
Tuple of (score points 0-100, score label).
"""
if total_sleep_seconds is None or total_sleep_seconds <= 0:
return (0, "POOR")
light_pct = _calculate_stage_percentage(light_sleep_seconds, total_sleep_seconds)
score = int(_score_sleep_stage(light_pct, 45.0, 55.0, 50.0))
if score >= 90:
return (score, "EXCELLENT")
if score >= 70:
return (score, "GOOD")
if score >= 50:
return (score, "FAIR")
return (score, "POOR")
def calculate_sleep_stress_score(
avg_sleep_stress: int | None,
restless_moments_count: int | None,
) -> tuple[int, str]:
"""
Calculate sleep stress score.
Based on average stress level and restless moments.
Garmin stress scale: 0-25 (rest), 26-50 (low), 51-75 (med),
76-100 (high).
Args:
avg_sleep_stress: Average stress level during sleep (0-100).
restless_moments_count: Count of restless moments.
Returns:
Tuple of (score points 0-100, score label).
"""
if avg_sleep_stress is None:
return (50, "FAIR")
stress_value = float(avg_sleep_stress)
# Base score from stress level (inverted: lower stress =
# higher score)
if stress_value <= 25: # Rest state
base_score = 100
elif stress_value <= 50: # Low stress
base_score = 90 - ((stress_value - 25) / 25 * 20)
elif stress_value <= 75: # Medium stress
base_score = 70 - ((stress_value - 50) / 25 * 20)
else: # High stress
base_score = 50 - ((stress_value - 75) / 25 * 50)
# Penalty for restless moments
restless_penalty = 0
if restless_moments_count is not None:
if restless_moments_count <= 5:
restless_penalty = restless_moments_count * 2
else:
restless_penalty = 10 + ((restless_moments_count - 5) * 3)
final_score = int(max(0, base_score - restless_penalty))
if final_score >= 90:
return (final_score, "EXCELLENT")
if final_score >= 70:
return (final_score, "GOOD")
if final_score >= 50:
return (final_score, "FAIR")
return (final_score, "POOR")
def calculate_sleep_score_overall(
sleep_start_time: datetime | None,
sleep_end_time: datetime | None,
total_sleep_seconds: int | None,
deep_sleep_seconds: int | None,
light_sleep_seconds: int | None,
rem_sleep_seconds: int | None,
awake_sleep_seconds: int | None,
awake_count: int | None,
restless_moments_count: int | None,
avg_sleep_stress: int | None,
) -> int:
"""
Calculate overall sleep score (0-100).
Combines multiple sleep metrics with weighted contributions:
- Duration score: 30%
- Quality score: 40%
- Awake count: 10%
- Stress score: 20%
Args:
sleep_start_time: Start time of sleep session.
sleep_end_time: End time of sleep session.
total_sleep_seconds: Total sleep duration in seconds.
deep_sleep_seconds: Duration of deep sleep in seconds.
light_sleep_seconds: Duration of light sleep in seconds.
rem_sleep_seconds: Duration of REM sleep in seconds.
awake_sleep_seconds: Duration of awake time in seconds.
awake_count: Number of times awakened during sleep.
restless_moments_count: Count of restless moments.
avg_sleep_stress: Average stress level during sleep.
Returns:
Overall sleep score from 0-100.
"""
# Calculate component scores
duration_score, _ = calculate_sleep_score_duration(
sleep_start_time, sleep_end_time, total_sleep_seconds
)
quality_score, _ = calculate_sleep_score_quality(
deep_sleep_seconds,
light_sleep_seconds,
rem_sleep_seconds,
awake_sleep_seconds,
total_sleep_seconds,
)
awake_score, _ = calculate_awake_count_score(awake_count)
stress_score, _ = calculate_sleep_stress_score(
avg_sleep_stress, restless_moments_count
)
# Weighted combination
overall_score = int(
(duration_score * 0.30)
+ (quality_score * 0.40)
+ (awake_score * 0.10)
+ (stress_score * 0.20)
)
return max(0, min(100, overall_score))
def _calculate_and_set_sleep_scores(
health_sleep: health_sleep_schema.HealthSleep,
) -> None:
"""
Calculate and set all sleep scores for a health sleep record.
This function computes sleep quality scores and updates the
health_sleep object in place with calculated values.
Args:
health_sleep: The health sleep record to update with scores.
Returns:
None: Updates health_sleep object in place.
"""
# Calculate overall sleep score
health_sleep.sleep_score_overall = calculate_sleep_score_overall(
health_sleep.sleep_start_time_local,
health_sleep.sleep_end_time_local,
health_sleep.total_sleep_seconds,
health_sleep.deep_sleep_seconds,
health_sleep.light_sleep_seconds,
health_sleep.rem_sleep_seconds,
health_sleep.awake_sleep_seconds,
health_sleep.awake_count,
health_sleep.restless_moments_count,
health_sleep.avg_sleep_stress,
)
# Calculate duration score
_, duration_label = calculate_sleep_score_duration(
health_sleep.sleep_start_time_local,
health_sleep.sleep_end_time_local,
health_sleep.total_sleep_seconds,
)
health_sleep.sleep_score_duration = health_sleep_schema.SleepScore(duration_label)
# Calculate quality score
_, quality_label = calculate_sleep_score_quality(
health_sleep.deep_sleep_seconds,
health_sleep.light_sleep_seconds,
health_sleep.rem_sleep_seconds,
health_sleep.awake_sleep_seconds,
health_sleep.total_sleep_seconds,
)
health_sleep.sleep_score_quality = health_sleep_schema.SleepScore(quality_label)
# Calculate awake count score
_, awake_label = calculate_awake_count_score(health_sleep.awake_count)
health_sleep.awake_count_score = health_sleep_schema.SleepScore(awake_label)
# Calculate REM percentage score
_, rem_label = calculate_rem_percentage_score(
health_sleep.rem_sleep_seconds,
health_sleep.total_sleep_seconds,
)
health_sleep.rem_percentage_score = health_sleep_schema.SleepScore(rem_label)
# Calculate deep percentage score
_, deep_label = calculate_deep_percentage_score(
health_sleep.deep_sleep_seconds,
health_sleep.total_sleep_seconds,
)
health_sleep.deep_percentage_score = health_sleep_schema.SleepScore(deep_label)
# Calculate light percentage score
_, light_label = calculate_light_percentage_score(
health_sleep.light_sleep_seconds,
health_sleep.total_sleep_seconds,
)
health_sleep.light_percentage_score = health_sleep_schema.SleepScore(light_label)
# Calculate sleep stress score
_, stress_label = calculate_sleep_stress_score(
health_sleep.avg_sleep_stress,
health_sleep.restless_moments_count,
)
health_sleep.sleep_stress_score = health_sleep_schema.SleepScore(stress_label)

View File

@@ -159,7 +159,7 @@ def get_health_steps_by_date(
return (
db.query(health_steps_models.HealthSteps)
.filter(
health_steps_models.HealthSteps.date == date,
health_steps_models.HealthSteps.date == func.date(date),
health_steps_models.HealthSteps.user_id == user_id,
)
.first()

View File

@@ -197,7 +197,7 @@ def get_health_weight_by_date(
return (
db.query(health_weight_models.HealthWeight)
.filter(
health_weight_models.HealthWeight.date == date,
health_weight_models.HealthWeight.date == func.date(date),
health_weight_models.HealthWeight.user_id == user_id,
)
.first()

View File

@@ -97,19 +97,17 @@ class TestHealthSleepModel:
# Assert
assert hasattr(health_sleep_models.HealthSleep, "user")
def test_health_sleep_model_decimal_precision(self):
def test_health_sleep_model_integer_fields(self):
"""
Test HealthSleep model decimal fields precision.
Test HealthSleep model integer fields.
"""
# Arrange
avg_heart_rate_column = health_sleep_models.HealthSleep.avg_heart_rate
avg_spo2_column = health_sleep_models.HealthSleep.avg_spo2
# Assert
assert avg_heart_rate_column.type.precision == 10
assert avg_heart_rate_column.type.scale == 2
assert avg_spo2_column.type.precision == 10
assert avg_spo2_column.type.scale == 2
assert avg_heart_rate_column.type.python_type == int
assert avg_spo2_column.type.python_type == int
def test_health_sleep_model_string_field_lengths(self):
"""

View File

@@ -37,10 +37,10 @@ class TestReadHealthSleepAll:
mock_sleep1.light_sleep_seconds = 14400
mock_sleep1.rem_sleep_seconds = 7200
mock_sleep1.awake_sleep_seconds = 0
mock_sleep1.avg_heart_rate = Decimal("55.5")
mock_sleep1.avg_heart_rate = 55
mock_sleep1.min_heart_rate = 45
mock_sleep1.max_heart_rate = 75
mock_sleep1.avg_spo2 = Decimal("97.5")
mock_sleep1.avg_spo2 = 97
mock_sleep1.lowest_spo2 = 95
mock_sleep1.highest_spo2 = 99
mock_sleep1.avg_respiration = None
@@ -297,6 +297,39 @@ class TestCreateHealthSleep:
assert response.status_code == 400
assert "Date field is required" in response.json()["detail"]
@patch("health_sleep.router.sleep_scoring._calculate_and_set_sleep_scores")
@patch("health_sleep.router.health_sleep_crud.create_health_sleep")
@patch("health_sleep.router.health_sleep_crud.get_health_sleep_by_date")
def test_create_health_sleep_calls_scoring(
self, mock_get_by_date, mock_create, mock_scoring, fast_api_client, fast_api_app
):
"""
Test creating health sleep calls sleep scoring function.
"""
# Arrange
mock_get_by_date.return_value = None
created_sleep = health_sleep_schema.HealthSleep(
id=1,
user_id=1,
date=datetime_date(2024, 1, 15),
total_sleep_seconds=28800,
)
mock_create.return_value = created_sleep
# Act
response = fast_api_client.post(
"/health_sleep",
json={
"date": "2024-01-15",
"total_sleep_seconds": 28800,
},
headers={"Authorization": "Bearer mock_token"},
)
# Assert
assert response.status_code == 201
mock_scoring.assert_called_once()
class TestEditHealthSleep:
"""
@@ -361,6 +394,44 @@ class TestEditHealthSleep:
assert response.status_code == 404
class TestEditHealthSleepScoringIntegration:
"""
Test suite for edit_health_sleep endpoint scoring integration.
"""
@patch("health_sleep.router.sleep_scoring._calculate_and_set_sleep_scores")
@patch("health_sleep.router.health_sleep_crud.edit_health_sleep")
def test_edit_health_sleep_calls_scoring(
self, mock_edit, mock_scoring, fast_api_client, fast_api_app
):
"""
Test editing health sleep calls sleep scoring function.
"""
# Arrange
updated_sleep = health_sleep_schema.HealthSleep(
id=1,
user_id=1,
date=datetime_date(2024, 1, 15),
total_sleep_seconds=32400,
)
mock_edit.return_value = updated_sleep
# Act
response = fast_api_client.put(
"/health_sleep",
json={
"id": 1,
"date": "2024-01-15",
"total_sleep_seconds": 32400,
},
headers={"Authorization": "Bearer mock_token"},
)
# Assert
assert response.status_code == 200
mock_scoring.assert_called_once()
class TestDeleteHealthSleep:
"""
Test suite for delete_health_sleep endpoint.

View File

@@ -26,7 +26,7 @@ class TestHealthSleepSchema:
deep_sleep_seconds=7200,
light_sleep_seconds=14400,
rem_sleep_seconds=7200,
avg_heart_rate=Decimal("55.5"),
avg_heart_rate=55,
min_heart_rate=45,
max_heart_rate=75,
sleep_score_overall=85,
@@ -109,10 +109,10 @@ class TestHealthSleepSchema:
light_sleep_seconds = 14400
rem_sleep_seconds = 7200
awake_sleep_seconds = 0
avg_heart_rate = Decimal("55.5")
avg_heart_rate = 55
min_heart_rate = 45
max_heart_rate = 75
avg_spo2 = Decimal("97.5")
avg_spo2 = 97
lowest_spo2 = 95
highest_spo2 = 99
avg_respiration = None
@@ -166,13 +166,13 @@ class TestHealthSleepSchema:
"""
# Arrange & Act
health_sleep = health_sleep_schema.HealthSleep(
avg_heart_rate=Decimal("60.5"),
avg_heart_rate=60,
min_heart_rate=45,
max_heart_rate=85,
)
# Assert
assert health_sleep.avg_heart_rate == Decimal("60.5")
assert health_sleep.avg_heart_rate == 60
assert health_sleep.min_heart_rate == 45
assert health_sleep.max_heart_rate == 85
@@ -202,13 +202,13 @@ class TestHealthSleepSchema:
"""
# Arrange & Act
health_sleep = health_sleep_schema.HealthSleep(
avg_spo2=Decimal("97.5"),
avg_spo2=97,
lowest_spo2=95,
highest_spo2=99,
)
# Assert
assert health_sleep.avg_spo2 == Decimal("97.5")
assert health_sleep.avg_spo2 == 97
assert health_sleep.lowest_spo2 == 95
assert health_sleep.highest_spo2 == 99

View File

@@ -0,0 +1,585 @@
import pytest
from datetime import datetime
from unittest.mock import MagicMock
import health_sleep.sleep_scoring as sleep_scoring
import health_sleep.schema as health_sleep_schema
class TestCalculateSleepDurationHours:
"""
Test suite for _calculate_sleep_duration_hours function.
"""
def test_calculate_sleep_duration_hours_success(self):
"""
Test converting seconds to hours.
"""
# Act
result = sleep_scoring._calculate_sleep_duration_hours(28800)
# Assert
assert result == 8.0
def test_calculate_sleep_duration_hours_none(self):
"""
Test converting None returns 0.
"""
# Act
result = sleep_scoring._calculate_sleep_duration_hours(None)
# Assert
assert result == 0.0
class TestCalculateStagePercentage:
"""
Test suite for _calculate_stage_percentage function.
"""
def test_calculate_stage_percentage_success(self):
"""
Test calculating stage percentage.
"""
# Act
result = sleep_scoring._calculate_stage_percentage(7200, 28800)
# Assert
assert result == 25.0
def test_calculate_stage_percentage_none_stage(self):
"""
Test calculating stage percentage with None stage.
"""
# Act
result = sleep_scoring._calculate_stage_percentage(None, 28800)
# Assert
assert result == 0.0
def test_calculate_stage_percentage_zero_total(self):
"""
Test calculating stage percentage with zero total.
"""
# Act
result = sleep_scoring._calculate_stage_percentage(7200, 0)
# Assert
assert result == 0.0
class TestCalculateSleepScoreDuration:
"""
Test suite for calculate_sleep_score_duration function.
"""
def test_calculate_sleep_score_duration_optimal_8_hours(self):
"""
Test duration score for optimal 8 hours sleep.
"""
# Arrange
start_time = datetime(2024, 1, 14, 22, 0, 0)
end_time = datetime(2024, 1, 15, 6, 0, 0)
total_seconds = 28800 # 8 hours
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(
start_time, end_time, total_seconds
)
# Assert
assert score == 100
assert label == "EXCELLENT"
def test_calculate_sleep_score_duration_7_hours(self):
"""
Test duration score for 7 hours sleep.
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(None, None, 25200)
# Assert
assert score == 90
assert label == "EXCELLENT"
def test_calculate_sleep_score_duration_6_hours(self):
"""
Test duration score for 6 hours sleep.
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(None, None, 21600)
# Assert
assert score == 70
assert label == "GOOD"
def test_calculate_sleep_score_duration_5_hours(self):
"""
Test duration score for 5 hours sleep.
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(None, None, 18000)
# Assert
assert score == 50
assert label == "FAIR"
def test_calculate_sleep_score_duration_4_hours(self):
"""
Test duration score for 4 hours sleep (poor).
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(None, None, 14400)
# Assert
assert label == "POOR"
assert score < 50
def test_calculate_sleep_score_duration_none(self):
"""
Test duration score with None total_sleep_seconds.
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_duration(None, None, None)
# Assert
assert score == 0
assert label == "POOR"
class TestCalculateSleepScoreQuality:
"""
Test suite for calculate_sleep_score_quality function.
"""
def test_calculate_sleep_score_quality_optimal(self):
"""
Test quality score with optimal sleep stages.
"""
# Arrange
total_seconds = 28800 # 8 hours
deep_seconds = 5184 # 18%
rem_seconds = 6480 # 22.5%
light_seconds = 14400 # 50%
awake_seconds = 720 # 2.5%
# Act
score, label = sleep_scoring.calculate_sleep_score_quality(
deep_seconds,
light_seconds,
rem_seconds,
awake_seconds,
total_seconds,
)
# Assert
assert score >= 90
assert label == "EXCELLENT"
def test_calculate_sleep_score_quality_none_total(self):
"""
Test quality score with None total sleep.
"""
# Act
score, label = sleep_scoring.calculate_sleep_score_quality(
None, None, None, None, None
)
# Assert
assert score == 0
assert label == "POOR"
def test_calculate_sleep_score_quality_poor_stages(self):
"""
Test quality score with poor sleep stage distribution.
"""
# Arrange
total_seconds = 28800
deep_seconds = 1440 # 5% (too low)
rem_seconds = 2880 # 10% (too low)
light_seconds = 21600 # 75% (too high)
awake_seconds = 2880 # 10% (too high)
# Act
score, label = sleep_scoring.calculate_sleep_score_quality(
deep_seconds,
light_seconds,
rem_seconds,
awake_seconds,
total_seconds,
)
# Assert
assert score < 70
assert label in ["FAIR", "POOR"]
class TestCalculateAwakeCountScore:
"""
Test suite for calculate_awake_count_score function.
"""
def test_calculate_awake_count_score_zero(self):
"""
Test awake count score with 0 awakenings.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(0)
# Assert
assert score == 100
assert label == "EXCELLENT"
def test_calculate_awake_count_score_one(self):
"""
Test awake count score with 1 awakening.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(1)
# Assert
assert score == 95
assert label == "EXCELLENT"
def test_calculate_awake_count_score_three(self):
"""
Test awake count score with 3 awakenings.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(3)
# Assert
assert score == 79
assert label == "GOOD"
def test_calculate_awake_count_score_five(self):
"""
Test awake count score with 5 awakenings.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(5)
# Assert
assert score == 59
assert label == "FAIR"
def test_calculate_awake_count_score_many(self):
"""
Test awake count score with many awakenings.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(10)
# Assert
assert score < 50
assert label == "POOR"
def test_calculate_awake_count_score_none(self):
"""
Test awake count score with None.
"""
# Act
score, label = sleep_scoring.calculate_awake_count_score(None)
# Assert
assert score == 50
assert label == "FAIR"
class TestCalculateRemPercentageScore:
"""
Test suite for calculate_rem_percentage_score function.
"""
def test_calculate_rem_percentage_score_optimal(self):
"""
Test REM percentage score with optimal value.
"""
# Arrange
total_seconds = 28800
rem_seconds = 6480 # 22.5%
# Act
score, label = sleep_scoring.calculate_rem_percentage_score(
rem_seconds, total_seconds
)
# Assert
assert score >= 90
assert label == "EXCELLENT"
def test_calculate_rem_percentage_score_good(self):
"""
Test REM percentage score with good value.
"""
# Arrange
total_seconds = 28800
rem_seconds = 6048 # 21%
# Act
score, label = sleep_scoring.calculate_rem_percentage_score(
rem_seconds, total_seconds
)
# Assert
assert score >= 80
assert label in ["EXCELLENT", "GOOD"]
def test_calculate_rem_percentage_score_none_total(self):
"""
Test REM percentage score with None total.
"""
# Act
score, label = sleep_scoring.calculate_rem_percentage_score(None, None)
# Assert
assert score == 0
assert label == "POOR"
class TestCalculateDeepPercentageScore:
"""
Test suite for calculate_deep_percentage_score function.
"""
def test_calculate_deep_percentage_score_optimal(self):
"""
Test deep percentage score with optimal value.
"""
# Arrange
total_seconds = 28800
deep_seconds = 5184 # 18%
# Act
score, label = sleep_scoring.calculate_deep_percentage_score(
deep_seconds, total_seconds
)
# Assert
assert score >= 90
assert label == "EXCELLENT"
def test_calculate_deep_percentage_score_none_total(self):
"""
Test deep percentage score with None total.
"""
# Act
score, label = sleep_scoring.calculate_deep_percentage_score(None, None)
# Assert
assert score == 0
assert label == "POOR"
class TestCalculateLightPercentageScore:
"""
Test suite for calculate_light_percentage_score function.
"""
def test_calculate_light_percentage_score_optimal(self):
"""
Test light percentage score with optimal value.
"""
# Arrange
total_seconds = 28800
light_seconds = 14400 # 50%
# Act
score, label = sleep_scoring.calculate_light_percentage_score(
light_seconds, total_seconds
)
# Assert
assert score >= 90
assert label == "EXCELLENT"
def test_calculate_light_percentage_score_none_total(self):
"""
Test light percentage score with None total.
"""
# Act
score, label = sleep_scoring.calculate_light_percentage_score(None, None)
# Assert
assert score == 0
assert label == "POOR"
class TestCalculateSleepStressScore:
"""
Test suite for calculate_sleep_stress_score function.
"""
def test_calculate_sleep_stress_score_low_stress(self):
"""
Test sleep stress score with low stress.
"""
# Act
score, label = sleep_scoring.calculate_sleep_stress_score(20, 2)
# Assert
assert score >= 90
assert label in ["EXCELLENT", "GOOD"]
def test_calculate_sleep_stress_score_medium_stress(self):
"""
Test sleep stress score with medium stress.
"""
# Act
score, label = sleep_scoring.calculate_sleep_stress_score(60, 3)
# Assert
assert score >= 50
assert label in ["GOOD", "FAIR"]
def test_calculate_sleep_stress_score_high_stress(self):
"""
Test sleep stress score with high stress.
"""
# Act
score, label = sleep_scoring.calculate_sleep_stress_score(85, 8)
# Assert
assert score < 50
assert label == "POOR"
def test_calculate_sleep_stress_score_none(self):
"""
Test sleep stress score with None stress.
"""
# Act
score, label = sleep_scoring.calculate_sleep_stress_score(None, None)
# Assert
assert score == 50
assert label == "FAIR"
class TestCalculateSleepScoreOverall:
"""
Test suite for calculate_sleep_score_overall function.
"""
def test_calculate_sleep_score_overall_excellent(self):
"""
Test overall sleep score with excellent sleep.
"""
# Arrange
start_time = datetime(2024, 1, 14, 22, 0, 0)
end_time = datetime(2024, 1, 15, 6, 0, 0)
total_seconds = 28800 # 8 hours
deep_seconds = 5184 # 18%
light_seconds = 14400 # 50%
rem_seconds = 6480 # 22.5%
awake_seconds = 720 # 2.5%
awake_count = 1
restless_count = 2
avg_stress = 20
# Act
score = sleep_scoring.calculate_sleep_score_overall(
start_time,
end_time,
total_seconds,
deep_seconds,
light_seconds,
rem_seconds,
awake_seconds,
awake_count,
restless_count,
avg_stress,
)
# Assert
assert score >= 85
assert score <= 100
def test_calculate_sleep_score_overall_poor(self):
"""
Test overall sleep score with poor sleep.
"""
# Arrange
start_time = datetime(2024, 1, 14, 23, 0, 0)
end_time = datetime(2024, 1, 15, 3, 0, 0)
total_seconds = 14400 # 4 hours
deep_seconds = 1440 # 10%
light_seconds = 10800 # 75%
rem_seconds = 1440 # 10%
awake_seconds = 720 # 5%
awake_count = 8
restless_count = 12
avg_stress = 85
# Act
score = sleep_scoring.calculate_sleep_score_overall(
start_time,
end_time,
total_seconds,
deep_seconds,
light_seconds,
rem_seconds,
awake_seconds,
awake_count,
restless_count,
avg_stress,
)
# Assert
assert score >= 0
assert score < 50
class TestCalculateAndSetSleepScores:
"""
Test suite for _calculate_and_set_sleep_scores function.
"""
def test_calculate_and_set_sleep_scores_success(self):
"""
Test calculating and setting all sleep scores.
"""
# Arrange
health_sleep = health_sleep_schema.HealthSleep(
sleep_start_time_local=datetime(2024, 1, 14, 22, 0, 0),
sleep_end_time_local=datetime(2024, 1, 15, 6, 0, 0),
total_sleep_seconds=28800,
deep_sleep_seconds=5184,
light_sleep_seconds=14400,
rem_sleep_seconds=6480,
awake_sleep_seconds=720,
awake_count=1,
restless_moments_count=2,
avg_sleep_stress=20,
)
# Act
sleep_scoring._calculate_and_set_sleep_scores(health_sleep)
# Assert
assert health_sleep.sleep_score_overall is not None
assert health_sleep.sleep_score_overall >= 0
assert health_sleep.sleep_score_overall <= 100
assert health_sleep.sleep_score_duration is not None
assert health_sleep.sleep_score_quality is not None
assert health_sleep.awake_count_score is not None
assert health_sleep.rem_percentage_score is not None
assert health_sleep.deep_percentage_score is not None
assert health_sleep.light_percentage_score is not None
assert health_sleep.sleep_stress_score is not None
def test_calculate_and_set_sleep_scores_minimal_data(self):
"""
Test calculating scores with minimal data.
"""
# Arrange
health_sleep = health_sleep_schema.HealthSleep(
total_sleep_seconds=28800,
)
# Act
sleep_scoring._calculate_and_set_sleep_scores(health_sleep)
# Assert
assert health_sleep.sleep_score_overall is not None
assert health_sleep.sleep_score_duration is not None
assert health_sleep.sleep_score_quality is not None

View File

@@ -0,0 +1,288 @@
# Sleep Scoring Guide
Endurain uses a sleep scoring system to help you understand the quality of your sleep. This guide explains how your sleep is evaluated and what the scores mean.
## What is Sleep Scoring?
Sleep scoring is like a report card for your sleep. The system analyzes your sleep data and gives you:
- **An overall sleep score** (0-100 points)
- **Individual component scores** with quality labels
- **Easy-to-understand ratings**: Excellent, Good, Fair, or Poor
## How Are Scores Calculated?
### Overall Sleep Score (0-100)
Your overall sleep score combines several important factors:
- **30%** - How long you slept (Duration)
- **40%** - How well you slept (Quality)
- **10%** - How many times you woke up
- **20%** - Your stress levels during sleep
**What the scores mean:**
- **90-100**: Excellent sleep! You're well-rested and recharged
- **70-89**: Good sleep. You got decent rest with room for improvement
- **50-69**: Fair sleep. You might feel somewhat rested but could do better
- **0-49**: Poor sleep. You likely feel tired and need better rest
---
## Understanding Each Score Component
### 1. Sleep Duration Score
**What it measures:** How many hours you slept
**Why it matters:** Your body needs enough time to go through all sleep stages and recover properly.
**Scoring guidelines:**
| Hours of Sleep | Score | Rating |
|----------------|-------|--------|
| 7-9 hours | 90-100 | **EXCELLENT** - Perfect amount! |
| 6-7 hours | 70-89 | **GOOD** - A bit short but okay |
| 9-10 hours | 70-89 | **GOOD** - A bit long but okay |
| 5-6 hours | 50-69 | **FAIR** - Not enough rest |
| 10-11 hours | 50-69 | **FAIR** - Possibly oversleeping |
| Less than 5 hours | 0-49 | **POOR** - Seriously insufficient |
| More than 11 hours | 0-49 | **POOR** - Too much sleep |
**The sweet spot:** 7-9 hours, with 8 hours being ideal for most adults.
---
### 2. Sleep Quality Score
**What it measures:** How your sleep was distributed across different sleep stages
**Why it matters:** Quality sleep isn't just about quantity - your brain and body need the right mix of sleep stages to fully recover.
**The four sleep stages:**
1. **Light Sleep** - Transition period, your body starts to relax
2. **Deep Sleep** - Body repairs muscles and tissues, strengthens immune system
3. **REM Sleep** - Brain processes memories and emotions, vivid dreams occur
4. **Awake** - Brief wakeful moments (some are normal!)
**Optimal percentages:**
| Sleep Stage | Ideal Range | Why It Matters |
|-------------|-------------|----------------|
| **Deep Sleep** | 13-23% (peak: 18%) | Physical recovery and healing |
| **REM Sleep** | 20-25% (peak: 22.5%) | Memory and emotional processing |
| **Light Sleep** | 45-55% (peak: 50%) | Transition between stages |
| **Awake Time** | Less than 5% | Normal brief awakenings |
**How it's scored:**
- Each sleep stage gets its own score based on how close you are to the optimal percentage
- The scores are combined with these weights:
- Deep sleep: 25%
- REM sleep: 30% (most important for mental recovery!)
- Light sleep: 25%
- Awake time: 20% (penalty applied)
---
### 3. Awake Count Score
**What it measures:** How many times you woke up during the night
**Why it matters:** Frequent awakenings disrupt your sleep cycles and prevent deep, restorative sleep.
**Scoring:**
| Awakenings | Score | Rating | What It Means |
|------------|-------|--------|---------------|
| 0-1 times | 90-100 | **EXCELLENT** | Uninterrupted, restorative sleep |
| 2-3 times | 70-89 | **GOOD** | Some interruptions but still decent |
| 4-5 times | 50-69 | **FAIR** | Sleep continuity affected |
| 6+ times | 0-49 | **POOR** | Very fragmented sleep |
**Note:** It's normal to wake up briefly 1-2 times per night. You might not even remember them!
---
### 4. REM Sleep Percentage Score
**What it measures:** What percentage of your sleep was REM (Rapid Eye Movement) sleep
**Why it matters:** REM sleep is when your brain consolidates memories, processes emotions, and boosts creativity. It's essential for mental health and learning.
**Optimal range:** 20-25% of total sleep time (about 1.5-2 hours for 8 hours of sleep)
**What different levels mean:**
- **Too low (<20%)**: May affect memory, mood, and cognitive function
- **Optimal (20-25%)**: Brain is fully processing and recovering
- **Too high (>25%)**: Unusual and may indicate sleep disorders
---
### 5. Deep Sleep Percentage Score
**What it measures:** What percentage of your sleep was deep sleep
**Why it matters:** Deep sleep is when your body does most of its physical repair - healing muscles, strengthening bones, and boosting your immune system.
**Optimal range:** 13-23% of total sleep time (about 1-2 hours for 8 hours of sleep)
**What different levels mean:**
- **Too low (<13%)**: Less physical recovery, may feel physically tired
- **Optimal (13-23%)**: Body is fully recovering and strengthening
- **Too high (>23%)**: Rare, but may indicate sleep debt recovery
**Did you know?** Deep sleep decreases naturally as you age, which is normal!
---
### 6. Light Sleep Percentage Score
**What it measures:** What percentage of your sleep was light sleep
**Why it matters:** Light sleep serves as a transition between sleep stages and makes up the largest portion of your sleep.
**Optimal range:** 45-55% of total sleep time (about 3.5-4.5 hours for 8 hours of sleep)
**What different levels mean:**
- **Too low (<45%)**: You may be spending too much time in other stages
- **Optimal (45-55%)**: Healthy balance of sleep stages
- **Too high (>55%)**: May not be getting enough deep or REM sleep
---
### 7. Sleep Stress Score
**What it measures:** Your average stress level during sleep and how restless you were
**Why it matters:** High stress during sleep indicates your body isn't fully relaxing, which affects recovery quality.
**Stress levels explained** (based on Garmin scale):
- **0-25**: Rest state - fully relaxed
- **26-50**: Low stress - mostly relaxed
- **51-75**: Medium stress - moderately elevated
- **76-100**: High stress - not relaxing properly
**Scoring:**
| Stress Level | Base Score | Rating |
|--------------|-----------|--------|
| 0-25 (Rest) | 100 | **EXCELLENT** |
| 26-50 (Low) | 70-90 | **GOOD** |
| 51-75 (Medium) | 50-70 | **FAIR** |
| 76-100 (High) | 0-50 | **POOR** |
**Restless moments penalty:** Each restless moment during sleep reduces your score by 2-3 points.
---
## Tips for Better Sleep Scores
### To Improve Duration Score:
- Aim for 7-9 hours of sleep per night
- Keep a consistent sleep schedule, even on weekends. Be consistent on the time you go to bed and awake not only the sleep hours
- Go to bed when you're tired, not too early or late
### To Improve Quality Score:
- Create a dark, quiet, cool sleeping environment
- Avoid screens and high intensity brain tasks 1 hour before bedtime (blue light disrupts sleep)
- Avoid caffeine 6 hours before bed
- Exercise regularly, but not right before bed
### To Reduce Awakenings:
- Limit fluid intake 2 hours before bedtime
- Keep your bedroom cool (60-67°F / 15-19°C is ideal)
- Use white noise or earplugs if needed
- Address any underlying sleep disorders with a doctor
### To Reduce Sleep Stress:
- Practice relaxation techniques before bed (meditation, deep breathing)
- Keep a regular exercise routine during the day
- Avoid stressful activities or conversations before bedtime
- Consider journaling to clear your mind before sleep
---
## Frequently Asked Questions
### Q: Why is my score low even though I slept 8 hours?
**A:** Duration is only 30% of your overall score. You might have had poor quality sleep, many awakenings, or high stress levels. Check your individual component scores to see what needs improvement.
### Q: Is it bad if my scores vary day to day?
**A:** Some variation is normal! Factors like stress, exercise, diet, and life events affect your sleep. Look for trends over weeks rather than individual nights.
### Q: What's more important - duration or quality?
**A:** Both matter! Quality is weighted slightly higher (40% vs 30%) because you can sleep for 10 hours but still feel tired if the quality is poor. Aim for both good duration AND quality.
### Q: My REM/Deep sleep percentage seems low. Is that bad?
**A:** Not necessarily. These percentages are averages based on research. Individual needs vary by age, genetics, and lifestyle. If you feel rested and energetic, your sleep is probably fine!
### Q: Can I compare my scores with friends?
**A:** While you can, remember that everyone's sleep needs are different. Focus on improving YOUR scores over time rather than comparing with others.
### Q: What if I have a sleep disorder?
**A:** These scores are educational tools, not medical diagnoses. If you consistently get poor scores or feel tired despite good scores, consult a healthcare professional or sleep specialist.
---
## Understanding Your Data
### When are scores calculated?
Scores are automatically calculated when you:
- Create a new sleep record
- Update an existing sleep record
- Important: If sleep data is imported from Garmin Connect, no calculations are made. Garmin Connect data is used
The system recalculates all scores to ensure they reflect your current data.
### What data is needed?
For the most accurate scores, provide:
- ✅ Sleep start and end times
- ✅ Total sleep duration
- ✅ Sleep stage breakdowns (deep, light, REM, awake)
- ✅ Number of awakenings
- ✅ Stress levels (optional but recommended)
**Partial data:** Even with incomplete data, the system will calculate scores for the metrics you provide. Missing metrics won't break the scoring system.
---
## Technical Details
For developers and technically-minded users:
- **Algorithm**: Based on sleep research (provided by Claude Sonnet 4.5 and double checked by me)
- **Scoring method**: Weighted average of normalized component scores
- **Data source**: Compatible with Garmin Connect and manual entry
- **Score range**: All scores normalized to 0-100 scale
- **Labels**: Threshold-based categorization (90+=Excellent, 70+=Good, 50+=Fair, <50=Poor)
---
## Summary
Your sleep scores provide valuable insights into your rest quality. By understanding what each score means and following the improvement tips, you can work towards better, more restorative sleep.
**Remember:**
- 🎯 Focus on trends, not single nights
- 💪 Small improvements add up over time
- 😴 Consistency is key to good sleep
- 🩺 Consult a doctor for persistent sleep issues
Sweet dreams and happy tracking! 🌙✨

View File

@@ -124,38 +124,21 @@
</div>
</div>
<!-- Unmeasurable Sleep -->
<div class="mb-3">
<label class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.unmeasurableSleepLabel') }}</b>
</label>
<div class="input-group">
<input id="unmeasurableSleepHours" v-model.number="formData.unmeasurableSleepHours" class="form-control"
type="number" :placeholder="$t('generalItems.labelHours')"
:aria-label="$t('generalItems.labelHours')" />
<span class="input-group-text">{{ $t('generalItems.labelHours') }}</span>
<input id="unmeasurableSleepMinutes" v-model.number="formData.unmeasurableSleepMinutes"
class="form-control" type="number" :placeholder="$t('generalItems.labelMinutes')"
:aria-label="$t('generalItems.labelMinutes')" />
<span class="input-group-text">{{ $t('generalItems.labelMinutes') }}</span>
</div>
</div>
<!-- Awake count section -->
<div class="row mb-3">
<div class="col-md-6">
<label for="awakeCount" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.awakeCountLabel') }}</b>
</label>
<input id="awakeCount" placeholder="..." class="form-control" type="number"
v-model.number="formData.awakeCount" />
<input id="awakeCount" :placeholder="$t('healthSleepAddEditModalComponent.awakeCountLabel')"
class="form-control" type="number" v-model.number="formData.awakeCount" />
</div>
<div class="col-md-6">
<label for="restlessMomentsCount" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.restlessMomentsCountLabel') }}</b>
<label for="avgSleepStress" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.avgSleepStressLabel') }}</b>
</label>
<input id="restlessMomentsCount" placeholder="..." class="form-control" type="number"
v-model.number="formData.restlessMomentsCount" />
<input id="avgSleepStress" :placeholder="$t('healthSleepAddEditModalComponent.avgSleepStressLabel')"
class="form-control" type="number" v-model.number="formData.avgSleepStress" />
</div>
</div>
@@ -166,7 +149,7 @@
<b>{{ $t('healthSleepAddEditModalComponent.avgHeartRateLabel') }}</b>
</label>
<input id="avgHeartRate" :placeholder="$t('healthSleepAddEditModalComponent.avgHeartRateLabel')"
class="form-control" type="number" step="0.01" v-model.number="formData.avgHeartRate" />
class="form-control" type="number" v-model.number="formData.avgHeartRate" />
</div>
<div class="col-md-4">
<label for="minHeartRate" class="form-label">
@@ -191,7 +174,7 @@
<b>{{ $t('healthSleepAddEditModalComponent.avgSpo2Label') }}</b>
</label>
<input id="avgSpo2" :placeholder="$t('healthSleepAddEditModalComponent.avgSpo2Label')"
class="form-control" type="number" step="0.01" v-model.number="formData.avgSpo2" />
class="form-control" type="number" v-model.number="formData.avgSpo2" />
</div>
<div class="col-md-4">
<label for="lowestSpo2" class="form-label">
@@ -230,12 +213,12 @@
</h6>
<button type="button" class="btn btn-sm btn-danger" @click="removeSleepStage(index)"
:aria-label="`Remove stage ${index + 1}`">
<i class="bi bi-trash"></i>
<font-awesome-icon :icon="['fas', 'trash']" />
</button>
</div>
<div class="row">
<div class="col-md-3">
<div class="col-md-4">
<label :for="`stageType${index}`" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.stageTypeLabel') }}</b>
</label>
@@ -255,7 +238,7 @@
</select>
</div>
<div class="col-md-3">
<div class="col-md-4">
<label :for="`stageStartTime${index}`" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.stageStartTimeLabel') }}</b>
</label>
@@ -263,20 +246,29 @@
v-model="stage.startTimeGmt" />
</div>
<div class="col-md-3">
<div class="col-md-4">
<label :for="`stageEndTime${index}`" class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.stageEndTimeLabel') }}</b>
</label>
<input :id="`stageEndTime${index}`" class="form-control" type="datetime-local"
v-model="stage.endTimeGmt" />
</div>
<div class="col-md-3">
<label :for="`stageDuration${index}`" class="form-label">
</div>
<div class="row mt-2">
<div class="col">
<label class="form-label">
<b>{{ $t('healthSleepAddEditModalComponent.stageDurationLabel') }}</b>
</label>
<input :id="`stageDuration${index}`" class="form-control" type="number"
v-model.number="stage.durationSeconds" placeholder="Seconds" />
<div class="input-group">
<input :id="`stageDurationHours${index}`" v-model.number="stage.durationHours"
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
:aria-label="$t('generalItems.labelHours')" />
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
<input :id="`stageDurationMinutes${index}`" v-model.number="stage.durationMinutes"
class="form-control" type="number" :placeholder="$t('generalItems.labelMinutes')"
:aria-label="$t('generalItems.labelMinutes')" />
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
</div>
</div>
</div>
</div>
@@ -318,7 +310,8 @@ interface SleepStage {
stageType: number | null
startTimeGmt: string | null
endTimeGmt: string | null
durationSeconds: number | null
durationHours: number | null
durationMinutes: number | null
}
interface SleepFormData {
@@ -327,8 +320,6 @@ interface SleepFormData {
sleepEndTime: string
totalSleepHours: number | null
totalSleepMinutes: number | null
unmeasurableSleepHours: number | null
unmeasurableSleepMinutes: number | null
deepSleepHours: number | null
deepSleepMinutes: number | null
lightSleepHours: number | null
@@ -344,7 +335,7 @@ interface SleepFormData {
lowestSpo2: number | null
highestSpo2: number | null
awakeCount: number | null
restlessMomentsCount: number | null
avgSleepStress: number | null
sleepStages: SleepStage[]
}
@@ -354,8 +345,8 @@ interface UserHealthSleep {
date: string
sleep_start_time_gmt: string
sleep_end_time_gmt: string
sleep_start_time_local?: string
sleep_end_time_local?: string
sleep_start_time_local: string
sleep_end_time_local: string
total_sleep_seconds?: number
unmeasurable_sleep_seconds?: number
deep_sleep_seconds?: number
@@ -369,7 +360,7 @@ interface UserHealthSleep {
lowest_spo2?: number
highest_spo2?: number
awake_count?: number
restless_moments_count?: number
avg_sleep_stress?: number
sleep_stages?: Array<{
stage_type: number | null
start_time_gmt: string | null
@@ -397,8 +388,6 @@ const formData = ref<SleepFormData>({
sleepEndTime: '',
totalSleepHours: null,
totalSleepMinutes: null,
unmeasurableSleepHours: null,
unmeasurableSleepMinutes: null,
deepSleepHours: null,
deepSleepMinutes: null,
lightSleepHours: null,
@@ -414,7 +403,7 @@ const formData = ref<SleepFormData>({
lowestSpo2: null,
highestSpo2: null,
awakeCount: null,
restlessMomentsCount: null,
avgSleepStress: null,
sleepStages: []
})
@@ -433,12 +422,10 @@ onMounted(() => {
formData.value = {
date: props.userHealthSleep.date,
sleepStartTime: formatDateTimeForInput(props.userHealthSleep.sleep_start_time_gmt),
sleepEndTime: formatDateTimeForInput(props.userHealthSleep.sleep_end_time_gmt),
sleepStartTime: formatDateTimeForInput(props.userHealthSleep.sleep_start_time_local),
sleepEndTime: formatDateTimeForInput(props.userHealthSleep.sleep_end_time_local),
totalSleepHours: totalSleep.hours,
totalSleepMinutes: totalSleep.minutes,
unmeasurableSleepHours: unmeasurableSleep.hours,
unmeasurableSleepMinutes: unmeasurableSleep.minutes,
deepSleepHours: deepSleep.hours,
deepSleepMinutes: deepSleep.minutes,
lightSleepHours: lightSleep.hours,
@@ -454,14 +441,18 @@ onMounted(() => {
lowestSpo2: props.userHealthSleep.lowest_spo2 ?? null,
highestSpo2: props.userHealthSleep.highest_spo2 ?? null,
awakeCount: props.userHealthSleep.awake_count ?? null,
restlessMomentsCount: props.userHealthSleep.restless_moments_count ?? null,
avgSleepStress: props.userHealthSleep.avg_sleep_stress ?? null,
sleepStages:
props.userHealthSleep.sleep_stages?.map((stage) => ({
stageType: stage.stage_type,
startTimeGmt: stage.start_time_gmt ? formatDateTimeForInput(stage.start_time_gmt) : null,
endTimeGmt: stage.end_time_gmt ? formatDateTimeForInput(stage.end_time_gmt) : null,
durationSeconds: stage.duration_seconds
})) ?? []
props.userHealthSleep.sleep_stages?.map((stage) => {
const duration = returnHoursMinutesFromSeconds(stage.duration_seconds ?? 0)
return {
stageType: stage.stage_type,
startTimeGmt: stage.start_time_gmt ? formatDateTimeForInput(stage.start_time_gmt) : null,
endTimeGmt: stage.end_time_gmt ? formatDateTimeForInput(stage.end_time_gmt) : null,
durationHours: duration.hours,
durationMinutes: duration.minutes
}
}) ?? []
}
editSleepId.value = `editSleepId${props.userHealthSleep.id}`
}
@@ -485,7 +476,8 @@ function addSleepStage(): void {
stageType: 1,
startTimeGmt: null,
endTimeGmt: null,
durationSeconds: null
durationHours: null,
durationMinutes: null
})
}
@@ -508,8 +500,8 @@ async function submitAddSleep(): Promise<void> {
try {
const data = {
date: formData.value.date,
sleep_start_time_gmt: formData.value.sleepStartTime,
sleep_end_time_gmt: formData.value.sleepEndTime,
sleep_start_time_local: formData.value.sleepStartTime,
sleep_end_time_local: formData.value.sleepEndTime,
total_sleep_seconds:
formData.value.totalSleepHours !== null && formData.value.totalSleepMinutes !== null
? returnSecondsFromHoursMinutes(
@@ -517,14 +509,6 @@ async function submitAddSleep(): Promise<void> {
formData.value.totalSleepMinutes
)
: null,
unmeasurable_sleep_seconds:
formData.value.unmeasurableSleepHours !== null &&
formData.value.unmeasurableSleepMinutes !== null
? returnSecondsFromHoursMinutes(
formData.value.unmeasurableSleepHours,
formData.value.unmeasurableSleepMinutes
)
: null,
deep_sleep_seconds:
formData.value.deepSleepHours !== null && formData.value.deepSleepMinutes !== null
? returnSecondsFromHoursMinutes(
@@ -560,12 +544,15 @@ async function submitAddSleep(): Promise<void> {
lowest_spo2: formData.value.lowestSpo2,
highest_spo2: formData.value.highestSpo2,
awake_count: formData.value.awakeCount,
restless_moments_count: formData.value.restlessMomentsCount,
avg_sleep_stress: formData.value.avgSleepStress,
sleep_stages: formData.value.sleepStages.map((stage) => ({
stage_type: stage.stageType,
start_time_gmt: stage.startTimeGmt,
end_time_gmt: stage.endTimeGmt,
duration_seconds: stage.durationSeconds
duration_seconds:
stage.durationHours !== null && stage.durationMinutes !== null
? returnSecondsFromHoursMinutes(stage.durationHours, stage.durationMinutes)
: null
}))
}
@@ -593,8 +580,8 @@ function submitEditSleep(): void {
id: props.userHealthSleep.id,
user_id: props.userHealthSleep.user_id,
date: formData.value.date,
sleep_start_time_gmt: formData.value.sleepStartTime,
sleep_end_time_gmt: formData.value.sleepEndTime,
sleep_start_time_local: formData.value.sleepStartTime,
sleep_end_time_local: formData.value.sleepEndTime,
total_sleep_seconds:
formData.value.totalSleepHours !== null && formData.value.totalSleepMinutes !== null
? returnSecondsFromHoursMinutes(
@@ -602,14 +589,6 @@ function submitEditSleep(): void {
formData.value.totalSleepMinutes
)
: null,
unmeasurable_sleep_seconds:
formData.value.unmeasurableSleepHours !== null &&
formData.value.unmeasurableSleepMinutes !== null
? returnSecondsFromHoursMinutes(
formData.value.unmeasurableSleepHours,
formData.value.unmeasurableSleepMinutes
)
: null,
deep_sleep_seconds:
formData.value.deepSleepHours !== null && formData.value.deepSleepMinutes !== null
? returnSecondsFromHoursMinutes(
@@ -645,12 +624,15 @@ function submitEditSleep(): void {
lowest_spo2: formData.value.lowestSpo2,
highest_spo2: formData.value.highestSpo2,
awake_count: formData.value.awakeCount,
restless_moments_count: formData.value.restlessMomentsCount,
avg_sleep_stress: formData.value.avgSleepStress,
sleep_stages: formData.value.sleepStages.map((stage) => ({
stage_type: stage.stageType,
start_time_gmt: stage.startTimeGmt,
end_time_gmt: stage.endTimeGmt,
duration_seconds: stage.durationSeconds
duration_seconds:
stage.durationHours !== null && stage.durationMinutes !== null
? returnSecondsFromHoursMinutes(stage.durationHours, stage.durationMinutes)
: null
}))
}

View File

@@ -18,7 +18,7 @@
"highestSpo2Label": "SpO2 máximo",
"sleepScoreOverallLabel": "Pontuação geral do sono",
"awakeCountLabel": "Número de despertares",
"restlessMomentsCountLabel": "Momentos inquietos",
"avgSleepStressLabel": "Stress Médio do Sono",
"sleepStagesLabel": "Fases do sono",
"addStageButton": "Adicionar fase",
"stageLabel": "Fase",
@@ -29,7 +29,7 @@
"stageTypeAwake": "Acordado",
"stageStartTimeLabel": "Hora de início",
"stageEndTimeLabel": "Hora de fim",
"stageDurationLabel": "Duração (segundos)",
"stageDurationLabel": "Duração",
"noStagesMessage": "Nenhuma fase de sono adicionada ainda. Clique em 'Adicionar fase' para adicionar uma fase de sono.",
"successAddSleep": "Sono adicionado com sucesso",
"errorAddSleep": "Erro ao adicionar sono"

View File

@@ -9,7 +9,6 @@
"lightSleepLabel": "Light sleep",
"remSleepLabel": "REM sleep",
"awakeSleepLabel": "Awake sleep",
"unmeasurableSleepLabel": "Unmeasurable sleep",
"avgHeartRateLabel": "Avg HR",
"minHeartRateLabel": "Min HR",
"maxHeartRateLabel": "Max HR",
@@ -18,7 +17,7 @@
"highestSpo2Label": "Max SpO2",
"sleepScoreOverallLabel": "Overall sleep score",
"awakeCountLabel": "Awake count",
"restlessMomentsCountLabel": "Restless moments",
"avgSleepStressLabel": "Avg Sleep Stress",
"sleepStagesLabel": "Sleep stages",
"addStageButton": "Add stage",
"stageLabel": "Stage",
@@ -29,7 +28,7 @@
"stageTypeAwake": "Awake",
"stageStartTimeLabel": "Start time",
"stageEndTimeLabel": "End time",
"stageDurationLabel": "Duration (seconds)",
"stageDurationLabel": "Duration",
"noStagesMessage": "No sleep stages added yet. Click 'Add stage' to add a sleep stage.",
"successAddSleep": "Sleep added successfully",
"errorAddSleep": "Error adding sleep"

View File

@@ -19,7 +19,7 @@
"goodLabel": "Good",
"fairLabel": "Fair",
"heartRateTitle": "Heart Rate",
"restingHeartRateLabel": "Resting",
"restingHeartRateLabel": "Resting HR",
"avgLabel": "Avg.",
"maxLabel": "Max.",
"minLabel": "Min.",
@@ -27,4 +27,4 @@
"spo2Title": "Blood Oxygen Saturation (SpO2)",
"avgSkinTempDeviationLabel": "Avg. Skin Temp. Deviation",
"avgSleepStressLabel": "Avg. Sleep Stress"
}
}

View File

@@ -2,79 +2,40 @@
<h1>{{ $t('healthView.title') }}</h1>
<div class="row row-gap-3">
<!-- Include the HealthSideBarComponent -->
<HealthSideBarComponent
:activeSection="activeSection"
@update-active-section="updateActiveSection"
/>
<HealthSideBarComponent :activeSection="activeSection" @update-active-section="updateActiveSection" />
<LoadingComponent v-if="isLoading" />
<!-- Include the HealthDashboardZone -->
<HealthDashboardZone
:userHealthWeight="userHealthWeight"
:userHealthSteps="userHealthSteps"
:userHealthSleep="userHealthSleep"
:userHealthTargets="userHealthTargets"
v-if="activeSection === 'dashboard' && !isLoading"
/>
<HealthDashboardZone :userHealthWeight="userHealthWeight" :userHealthSteps="userHealthSteps"
:userHealthSleep="userHealthSleep" :userHealthTargets="userHealthTargets"
v-if="activeSection === 'dashboard' && !isLoading" />
<!-- Include the HealthSleepZone -->
<HealthSleepZone
:userHealthSleep="userHealthSleep"
:userHealthSleepPagination="userHealthSleepPagination"
:userHealthTargets="userHealthTargets"
:isLoading="isLoading"
:totalPages="totalPagesSleep"
:pageNumber="pageNumberSleep"
@deletedSleep="updateSleepListDeleted"
@editedSleep="updateSleepListEdited"
@pageNumberChanged="setPageNumberSleep"
@setSleepTarget="setSleepTarget"
v-if="activeSection === 'sleep' && !isLoading"
/>
<HealthSleepZone :userHealthSleep="userHealthSleep" :userHealthSleepPagination="userHealthSleepPagination"
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSleep"
:pageNumber="pageNumberSleep" @createdSleep="updateSleepListAdded" @editedSleep="updateSleepListEdited"
@deletedSleep="updateSleepListDeleted" @pageNumberChanged="setPageNumberSleep" @setSleepTarget="setSleepTarget"
v-if="activeSection === 'sleep' && !isLoading" />
<!-- Include the HealthRHRZone -->
<HealthRHRZone
:userHealthSleep="userHealthSleep"
:userHealthSleepPagination="userHealthSleepPagination"
:isLoading="isLoading"
:totalPages="totalPagesRHR"
:pageNumber="pageNumberRHR"
@pageNumberChanged="setPageNumberRHR"
v-if="activeSection === 'rhr' && !isLoading"
/>
<HealthRHRZone :userHealthSleep="userHealthSleep" :userHealthSleepPagination="userHealthSleepPagination"
:isLoading="isLoading" :totalPages="totalPagesRHR" :pageNumber="pageNumberRHR"
@pageNumberChanged="setPageNumberRHR" v-if="activeSection === 'rhr' && !isLoading" />
<!-- Include the HealthStepsZone -->
<HealthStepsZone
:userHealthSteps="userHealthSteps"
:userHealthStepsPagination="userHealthStepsPagination"
:userHealthTargets="userHealthTargets"
:isLoading="isLoading"
:totalPages="totalPagesSteps"
:pageNumber="pageNumberSteps"
@createdSteps="updateStepsListAdded"
@deletedSteps="updateStepsListDeleted"
@editedSteps="updateStepsListEdited"
@pageNumberChanged="setPageNumberSteps"
@setStepsTarget="setStepsTarget"
v-if="activeSection === 'steps' && !isLoading"
/>
<HealthStepsZone :userHealthSteps="userHealthSteps" :userHealthStepsPagination="userHealthStepsPagination"
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSteps"
:pageNumber="pageNumberSteps" @createdSteps="updateStepsListAdded" @deletedSteps="updateStepsListDeleted"
@editedSteps="updateStepsListEdited" @pageNumberChanged="setPageNumberSteps" @setStepsTarget="setStepsTarget"
v-if="activeSection === 'steps' && !isLoading" />
<!-- Include the HealthWeightZone -->
<HealthWeightZone
:userHealthWeight="userHealthWeight"
:userHealthWeightPagination="userHealthWeightPagination"
:userHealthTargets="userHealthTargets"
:isLoading="isLoading"
:totalPages="totalPagesWeight"
:pageNumber="pageNumberWeight"
@createdWeight="updateWeightListAdded"
@deletedWeight="updateWeightListDeleted"
@editedWeight="updateWeightListEdited"
@pageNumberChanged="setPageNumberWeight"
@setWeightTarget="setWeightTarget"
v-if="activeSection === 'weight' && !isLoading"
/>
<HealthWeightZone :userHealthWeight="userHealthWeight" :userHealthWeightPagination="userHealthWeightPagination"
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesWeight"
:pageNumber="pageNumberWeight" @createdWeight="updateWeightListAdded" @deletedWeight="updateWeightListDeleted"
@editedWeight="updateWeightListEdited" @pageNumberChanged="setPageNumberWeight" @setWeightTarget="setWeightTarget"
v-if="activeSection === 'weight' && !isLoading" />
</div>
<!-- back button -->
<BackButtonComponent />
@@ -167,6 +128,28 @@ async function fetchHealthSleep() {
}
}
function updateSleepListAdded(createdSleep) {
const updateOrAdd = (array, newEntry) => {
const index = array.findIndex((item) => item.id === newEntry.id)
if (index !== -1) {
array[index] = newEntry
} else {
array.unshift(newEntry)
}
}
if (userHealthSleepPagination.value) {
updateOrAdd(userHealthSleepPagination.value, createdSleep)
} else {
userHealthSleepPagination.value = [createdSleep]
}
if (userHealthSleep.value) {
updateOrAdd(userHealthSleep.value, createdSleep)
} else {
userHealthSleep.value = [createdSleep]
}
userHealthSleepNumber.value = userHealthSleep.value.length
}
function updateSleepListEdited(editedSleep) {
const indexPagination = userHealthSleepPagination.value.findIndex(
(sleep) => sleep.id === editedSleep.id