Add body battery: sync, storage, and health UI chart
Parses Garmin Connect get_body_battery() per day, storing charged/drained/ start+end levels and the fine-grained [[ts_ms, level, type, stress]] values array in a new body_battery JSONB column on health_metrics. Frontend adds: - BatteryRing SVG gauge (color-scaled 0–100) - BodyBatteryChart: ComposedChart with type-colored bars (REST/ACTIVE/SLEEP/ STRESS) and battery level overlay line, matching Garmin's layout - Body battery trend chart in the Trends section (end_level per day) Also adds avg_hr_day and weight data which now correctly sync with the intraday_hr JSON serialization fix from the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc, func
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, model_validator
|
||||
from typing import Optional, List, Any
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -44,6 +44,13 @@ class HealthMetricOut(BaseModel):
|
||||
active_calories: Optional[float]
|
||||
total_calories: Optional[float]
|
||||
spo2_avg: Optional[float]
|
||||
body_battery: Optional[Any] = None # {charged,drained,start_level,end_level} — values stripped
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _strip_bb_values(self):
|
||||
if isinstance(self.body_battery, dict):
|
||||
self.body_battery = {k: v for k, v in self.body_battery.items() if k != 'values'}
|
||||
return self
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -136,7 +143,10 @@ async def intraday_health(
|
||||
)
|
||||
)
|
||||
metric = result.scalar_one_or_none()
|
||||
return {"hr_values": metric.intraday_hr if metric else None}
|
||||
return {
|
||||
"hr_values": metric.intraday_hr if metric else None,
|
||||
"body_battery": metric.body_battery if metric else None,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/manual")
|
||||
|
||||
Reference in New Issue
Block a user