HRV baseline band from Garmin + dashboard HRV colours + route name on recent activities + sync-now race fix
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
@@ -28,6 +29,7 @@ class ActivitySummary(BaseModel):
|
||||
bounding_box: Optional[dict]
|
||||
hr_zones: Optional[dict]
|
||||
named_route_id: Optional[int]
|
||||
named_route_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -110,7 +112,7 @@ async def list_activities(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
q = select(Activity).where(Activity.user_id == current_user.id)
|
||||
q = select(Activity).options(selectinload(Activity.named_route)).where(Activity.user_id == current_user.id)
|
||||
|
||||
if sport_type:
|
||||
q = q.where(Activity.sport_type == sport_type)
|
||||
@@ -133,7 +135,7 @@ async def get_activity(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Activity).where(
|
||||
select(Activity).options(selectinload(Activity.named_route)).where(
|
||||
Activity.id == activity_id,
|
||||
Activity.user_id == current_user.id,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@ class HealthMetricOut(BaseModel):
|
||||
hrv_status: Optional[str]
|
||||
hrv_5min_high: Optional[float]
|
||||
hrv_5min_low: Optional[float]
|
||||
hrv_baseline_low: Optional[float]
|
||||
hrv_baseline_upper: Optional[float]
|
||||
sleep_duration_s: Optional[float]
|
||||
sleep_deep_s: Optional[float]
|
||||
sleep_light_s: Optional[float]
|
||||
|
||||
@@ -68,6 +68,8 @@ async def init_db():
|
||||
"ALTER TABLE health_metrics ADD COLUMN IF NOT EXISTS intraday_hr JSONB",
|
||||
"ALTER TABLE health_metrics ADD COLUMN IF NOT EXISTS body_battery JSONB",
|
||||
"ALTER TABLE health_metrics ADD COLUMN IF NOT EXISTS sleep_stages JSON",
|
||||
"ALTER TABLE health_metrics ADD COLUMN IF NOT EXISTS hrv_baseline_low FLOAT",
|
||||
"ALTER TABLE health_metrics ADD COLUMN IF NOT EXISTS hrv_baseline_upper FLOAT",
|
||||
]:
|
||||
await conn.execute(text(stmt))
|
||||
except Exception as e:
|
||||
|
||||
@@ -125,6 +125,16 @@ class Activity(Base):
|
||||
named_route = relationship("NamedRoute", back_populates="activities")
|
||||
laps = relationship("ActivityLap", back_populates="activity", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def named_route_name(self):
|
||||
"""Name of the associated NamedRoute, or None. Reads the relationship only
|
||||
if it was eager-loaded (selectinload) so it never triggers a lazy load in
|
||||
the async request context — returns None when unloaded."""
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
if "named_route" in sa_inspect(self).unloaded:
|
||||
return None
|
||||
return self.named_route.name if self.named_route else None
|
||||
|
||||
|
||||
class ActivityDataPoint(Base):
|
||||
__tablename__ = "activity_data_points"
|
||||
@@ -254,6 +264,8 @@ class HealthMetric(Base):
|
||||
hrv_nightly_avg = Column(Float, nullable=True)
|
||||
hrv_5min_high = Column(Float, nullable=True)
|
||||
hrv_5min_low = Column(Float, nullable=True)
|
||||
hrv_baseline_low = Column(Float, nullable=True) # Garmin balanced range lower bound (balancedLow)
|
||||
hrv_baseline_upper = Column(Float, nullable=True) # Garmin balanced range upper bound (balancedUpper)
|
||||
sleep_duration_s = Column(Float, nullable=True)
|
||||
sleep_deep_s = Column(Float, nullable=True)
|
||||
sleep_light_s = Column(Float, nullable=True)
|
||||
|
||||
@@ -602,6 +602,10 @@ def _parse_day(stats, sleep_data, hrv_data) -> dict:
|
||||
status = summary.get("status")
|
||||
if status:
|
||||
row["hrv_status"] = str(status).lower()
|
||||
# Garmin's per-day balanced baseline range (the grey band in the app).
|
||||
baseline = summary.get("baseline") or {}
|
||||
_set(row, "hrv_baseline_low", baseline.get("balancedLow"))
|
||||
_set(row, "hrv_baseline_upper", baseline.get("balancedUpper"))
|
||||
|
||||
return row
|
||||
|
||||
|
||||
Reference in New Issue
Block a user