216 lines
8.4 KiB
Python
216 lines
8.4 KiB
Python
"""Health check and metric endpoints for API v1."""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.responses import FileResponse
|
|
import os
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
from typing import List, Optional
|
|
import datetime
|
|
|
|
from backend.database import get_db
|
|
from backend.models.daily_health_metrics import DailyHealthMetrics
|
|
from backend.models.user import User
|
|
from backend.models.workout_session import WorkoutSession
|
|
from backend.schemas.daily_health_metrics import DailyHealthMetricsResponse
|
|
from backend import auth
|
|
|
|
router = APIRouter(prefix="/api/health", tags=["health"])
|
|
|
|
|
|
@router.get("", status_code=status.HTTP_200_OK)
|
|
def health_check(db: Session = Depends(get_db)):
|
|
"""Check if the application and database are healthy."""
|
|
try:
|
|
# Check database connection
|
|
db.execute(text("SELECT 1"))
|
|
return {
|
|
"status": "healthy",
|
|
"database": "connected"
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=503, detail=f"Unhealthy: {str(e)}")
|
|
|
|
|
|
@router.get("/stats", response_model=List[DailyHealthMetricsResponse])
|
|
def get_health_stats(
|
|
period: str = "week",
|
|
ref_date: Optional[datetime.date] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(auth.get_current_user)
|
|
):
|
|
"""Retrieve health statistics for various periods."""
|
|
if ref_date is None:
|
|
ref_date = datetime.date.today()
|
|
|
|
if period == "day":
|
|
return db.query(DailyHealthMetrics).filter(
|
|
DailyHealthMetrics.user_id == current_user.id,
|
|
DailyHealthMetrics.date == ref_date
|
|
).all()
|
|
|
|
elif period == "week":
|
|
start_date = ref_date - datetime.timedelta(days=6)
|
|
return db.query(DailyHealthMetrics).filter(
|
|
DailyHealthMetrics.user_id == current_user.id,
|
|
DailyHealthMetrics.date.between(start_date, ref_date)
|
|
).order_by(DailyHealthMetrics.date.asc()).all()
|
|
|
|
elif period == "month":
|
|
start_date = ref_date - datetime.timedelta(days=29)
|
|
raw_data = db.query(DailyHealthMetrics).filter(
|
|
DailyHealthMetrics.user_id == current_user.id,
|
|
DailyHealthMetrics.date.between(start_date, ref_date)
|
|
).order_by(DailyHealthMetrics.date.asc()).all()
|
|
|
|
aggregated = []
|
|
import itertools
|
|
key_func = lambda x: x.date.isocalendar()[1]
|
|
|
|
for key, group in itertools.groupby(raw_data, key_func):
|
|
g = list(group)
|
|
first = g[0]
|
|
agg = {
|
|
"id": key,
|
|
"user_id": current_user.id,
|
|
"date": first.date,
|
|
"step_count": sum(x.step_count for x in g) // len(g),
|
|
"step_count_total": sum(x.step_count for x in g),
|
|
"calories_burned": sum(x.calories_burned for x in g) / len(g),
|
|
"distance_meters": sum(x.distance_meters for x in g) / len(g),
|
|
"sleep_duration_minutes": sum(x.sleep_duration_minutes for x in g) // len(g),
|
|
"deep_sleep_minutes": sum(x.deep_sleep_minutes for x in g) // len(g),
|
|
"light_sleep_minutes": sum(getattr(x, 'light_sleep_minutes', 0) for x in g) // len(g),
|
|
"rem_sleep_minutes": sum(getattr(x, 'rem_sleep_minutes', 0) for x in g) // len(g),
|
|
"awake_duration_minutes": sum(x.awake_duration_minutes for x in g) // len(g),
|
|
"pai_score": sum(x.pai_score for x in g) / len(g),
|
|
"avg_heart_rate": sum(x.avg_heart_rate or 0 for x in g) // len(g),
|
|
"avg_spo2": sum(x.avg_spo2 or 0 for x in g) / len(g),
|
|
"min_heart_rate": min((x.min_heart_rate for x in g if x.min_heart_rate), default=0),
|
|
"max_heart_rate": max((x.max_heart_rate for x in g if x.max_heart_rate), default=0)
|
|
}
|
|
aggregated.append(agg)
|
|
|
|
return aggregated
|
|
|
|
elif period == "year":
|
|
start_date = datetime.date.today() - datetime.timedelta(days=365)
|
|
raw_data = db.query(DailyHealthMetrics).filter(
|
|
DailyHealthMetrics.user_id == current_user.id,
|
|
DailyHealthMetrics.date >= start_date
|
|
).order_by(DailyHealthMetrics.date.asc()).all()
|
|
|
|
aggregated = []
|
|
import itertools
|
|
key_func = lambda x: x.date.month
|
|
|
|
for key, group in itertools.groupby(raw_data, key_func):
|
|
g = list(group)
|
|
first = g[0]
|
|
agg = {
|
|
"id": key,
|
|
"user_id": current_user.id,
|
|
"date": first.date.replace(day=1),
|
|
"step_count": sum(x.step_count for x in g) // len(g),
|
|
"step_count_total": sum(x.step_count for x in g),
|
|
"calories_burned": sum(x.calories_burned for x in g) / len(g),
|
|
"distance_meters": sum(x.distance_meters for x in g) / len(g),
|
|
"sleep_duration_minutes": sum(x.sleep_duration_minutes for x in g) // len(g),
|
|
"deep_sleep_minutes": sum(x.deep_sleep_minutes for x in g) // len(g),
|
|
"light_sleep_minutes": sum(getattr(x, 'light_sleep_minutes', 0) for x in g) // len(g),
|
|
"rem_sleep_minutes": sum(getattr(x, 'rem_sleep_minutes', 0) for x in g) // len(g),
|
|
"awake_duration_minutes": sum(x.awake_duration_minutes for x in g) // len(g),
|
|
"pai_score": sum(x.pai_score for x in g) / len(g),
|
|
"avg_heart_rate": sum(x.avg_heart_rate or 0 for x in g) // len(g),
|
|
"avg_spo2": sum(x.avg_spo2 or 0 for x in g) / len(g),
|
|
"min_heart_rate": min((x.min_heart_rate for x in g if x.min_heart_rate), default=0),
|
|
"max_heart_rate": max((x.max_heart_rate for x in g if x.max_heart_rate), default=0)
|
|
}
|
|
aggregated.append(agg)
|
|
|
|
return aggregated
|
|
|
|
return []
|
|
|
|
|
|
@router.get("/workouts")
|
|
def get_workouts(
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
activity_type: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(auth.get_current_user)
|
|
):
|
|
"""Retrieve workout sessions for the current user."""
|
|
query = db.query(WorkoutSession).filter(
|
|
WorkoutSession.user_id == current_user.id
|
|
)
|
|
|
|
if activity_type:
|
|
query = query.filter(WorkoutSession.activity_type == activity_type)
|
|
|
|
workouts = query.order_by(
|
|
WorkoutSession.start_time.desc()
|
|
).offset(offset).limit(limit).all()
|
|
|
|
# Manually serialize to avoid Pydantic issues
|
|
return [
|
|
{
|
|
"id": w.id,
|
|
"user_id": w.user_id,
|
|
"start_time": w.start_time.isoformat() if w.start_time else None,
|
|
"end_time": w.end_time.isoformat() if w.end_time else None,
|
|
"activity_type": w.activity_type,
|
|
"activity_type_id": w.activity_type_id,
|
|
"duration_seconds": w.duration_seconds or 0,
|
|
"distance_meters": w.distance_meters or 0.0,
|
|
"calories": w.calories or 0.0,
|
|
"avg_pace": w.avg_pace,
|
|
"max_pace": w.max_pace,
|
|
"min_pace": w.min_pace,
|
|
"avg_hr": w.avg_hr,
|
|
"max_hr": w.max_hr,
|
|
"min_hr": w.min_hr,
|
|
"notes": w.notes
|
|
}
|
|
for w in workouts
|
|
]
|
|
|
|
|
|
@router.get("/workouts/summary")
|
|
def get_workout_summary(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(auth.get_current_user)
|
|
):
|
|
"""Get summary statistics for workouts."""
|
|
from sqlalchemy import func
|
|
|
|
# Total counts
|
|
total = db.query(func.count(WorkoutSession.id)).filter(
|
|
WorkoutSession.user_id == current_user.id
|
|
).scalar()
|
|
|
|
# By activity type
|
|
by_type = db.query(
|
|
WorkoutSession.activity_type,
|
|
func.count(WorkoutSession.id).label('count'),
|
|
func.sum(WorkoutSession.duration_seconds).label('total_duration'),
|
|
func.sum(WorkoutSession.calories).label('total_calories'),
|
|
func.sum(WorkoutSession.distance_meters).label('total_distance')
|
|
).filter(
|
|
WorkoutSession.user_id == current_user.id
|
|
).group_by(WorkoutSession.activity_type).all()
|
|
|
|
return {
|
|
"total_workouts": total,
|
|
"by_activity_type": [
|
|
{
|
|
"activity_type": row[0],
|
|
"count": row[1],
|
|
"total_duration_seconds": row[2] or 0,
|
|
"total_calories": row[3] or 0,
|
|
"total_distance_meters": row[4] or 0
|
|
}
|
|
for row in by_type
|
|
]
|
|
}
|