HRV baseline band from Garmin + dashboard HRV colours + route name on recent activities + sync-now race fix
Build and push images / validate (push) Successful in 7s
Build and push images / build-backend (push) Successful in 1m9s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s

This commit is contained in:
2026-06-16 00:13:16 +01:00
parent e7123ee5db
commit d01f66223b
10 changed files with 98 additions and 45 deletions
+4 -2
View File
@@ -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,
)
+2
View File
@@ -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]
+2
View File
@@ -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:
+12
View File
@@ -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