fix
This commit is contained in:
@@ -5,24 +5,34 @@ from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.core.auth import get_current_rider
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.core.database import get_session
|
||||
from backend.app.models.activity import Activity, DataPoint
|
||||
from backend.app.models.activity import Activity, ActivityMetrics, DataPoint, Interval
|
||||
from backend.app.models.fitness import PowerCurve
|
||||
from backend.app.models.rider import Rider
|
||||
from backend.app.schemas.activity import (
|
||||
ActivityResponse,
|
||||
ActivityListResponse,
|
||||
DataPointResponse,
|
||||
ZonesResponse,
|
||||
PowerCurveResponse,
|
||||
)
|
||||
from backend.app.models.fitness import DiaryEntry
|
||||
from backend.app.services.fit_parser import parse_fit_file
|
||||
from backend.app.services.metrics import calculate_metrics
|
||||
from backend.app.services.zones import calculate_power_zones, calculate_hr_zones
|
||||
from backend.app.services.power_curve import calculate_power_curve
|
||||
from backend.app.services.intervals import detect_intervals
|
||||
from backend.app.services.ai_summary import generate_summary
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ActivityResponse)
|
||||
async def upload_activity(
|
||||
rider_id: uuid.UUID,
|
||||
file: UploadFile = File(...),
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
if not file.filename or not file.filename.lower().endswith(".fit"):
|
||||
@@ -37,18 +47,33 @@ async def upload_activity(
|
||||
content = await file.read()
|
||||
file_path.write_bytes(content)
|
||||
|
||||
activity, data_points = parse_fit_file(content, rider_id, str(file_path))
|
||||
# 1. Parse FIT
|
||||
activity, data_points = parse_fit_file(content, rider.id, str(file_path))
|
||||
session.add(activity)
|
||||
await session.flush()
|
||||
|
||||
# 2. Save data points
|
||||
for dp in data_points:
|
||||
dp.activity_id = activity.id
|
||||
session.add_all(data_points)
|
||||
|
||||
metrics = calculate_metrics(data_points, activity, rider_id, session)
|
||||
# 3. Calculate & save metrics (with FTP if available)
|
||||
metrics = calculate_metrics(data_points, activity, ftp=rider.ftp)
|
||||
if metrics:
|
||||
session.add(metrics)
|
||||
|
||||
# 4. Detect & save intervals
|
||||
intervals = detect_intervals(data_points, ftp=rider.ftp)
|
||||
for interval in intervals:
|
||||
interval.activity_id = activity.id
|
||||
session.add_all(intervals)
|
||||
|
||||
# 5. Calculate & save power curve
|
||||
curve_data = calculate_power_curve(data_points)
|
||||
if curve_data:
|
||||
pc = PowerCurve(activity_id=activity.id, curve_data=curve_data)
|
||||
session.add(pc)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(activity)
|
||||
return activity
|
||||
@@ -56,17 +81,17 @@ async def upload_activity(
|
||||
|
||||
@router.get("", response_model=ActivityListResponse)
|
||||
async def list_activities(
|
||||
rider_id: uuid.UUID,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
count_query = select(func.count(Activity.id)).where(Activity.rider_id == rider_id)
|
||||
count_query = select(func.count(Activity.id)).where(Activity.rider_id == rider.id)
|
||||
total = (await session.execute(count_query)).scalar() or 0
|
||||
|
||||
query = (
|
||||
select(Activity)
|
||||
.where(Activity.rider_id == rider_id)
|
||||
.where(Activity.rider_id == rider.id)
|
||||
.order_by(Activity.date.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
@@ -80,10 +105,11 @@ async def list_activities(
|
||||
@router.get("/{activity_id}", response_model=ActivityResponse)
|
||||
async def get_activity(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity:
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
return activity
|
||||
|
||||
@@ -91,8 +117,14 @@ async def get_activity(
|
||||
@router.get("/{activity_id}/stream", response_model=list[DataPointResponse])
|
||||
async def get_activity_stream(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
# Verify ownership
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
query = (
|
||||
select(DataPoint)
|
||||
.where(DataPoint.activity_id == activity_id)
|
||||
@@ -100,3 +132,173 @@ async def get_activity_stream(
|
||||
)
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{activity_id}/zones", response_model=ZonesResponse)
|
||||
async def get_activity_zones(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
query = (
|
||||
select(DataPoint)
|
||||
.where(DataPoint.activity_id == activity_id)
|
||||
.order_by(DataPoint.timestamp)
|
||||
)
|
||||
result = await session.execute(query)
|
||||
data_points = list(result.scalars().all())
|
||||
|
||||
power_zones = []
|
||||
hr_zones = []
|
||||
|
||||
if rider.ftp:
|
||||
power_zones = calculate_power_zones(data_points, rider.ftp)
|
||||
if rider.lthr:
|
||||
hr_zones = calculate_hr_zones(data_points, rider.lthr)
|
||||
|
||||
return ZonesResponse(power_zones=power_zones, hr_zones=hr_zones)
|
||||
|
||||
|
||||
@router.get("/{activity_id}/power-curve", response_model=PowerCurveResponse)
|
||||
async def get_activity_power_curve(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
# Try cached
|
||||
query = select(PowerCurve).where(PowerCurve.activity_id == activity_id)
|
||||
result = await session.execute(query)
|
||||
pc = result.scalar_one_or_none()
|
||||
|
||||
if pc:
|
||||
return PowerCurveResponse(curve=pc.curve_data)
|
||||
|
||||
# Calculate on the fly
|
||||
dp_query = (
|
||||
select(DataPoint)
|
||||
.where(DataPoint.activity_id == activity_id)
|
||||
.order_by(DataPoint.timestamp)
|
||||
)
|
||||
dp_result = await session.execute(dp_query)
|
||||
data_points = list(dp_result.scalars().all())
|
||||
curve = calculate_power_curve(data_points)
|
||||
|
||||
return PowerCurveResponse(curve=curve)
|
||||
|
||||
|
||||
@router.post("/{activity_id}/ai-summary")
|
||||
async def generate_ai_summary(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
# Check for existing diary entry with summary
|
||||
query = select(DiaryEntry).where(DiaryEntry.activity_id == activity_id)
|
||||
result = await session.execute(query)
|
||||
diary = result.scalar_one_or_none()
|
||||
|
||||
if diary and diary.ai_summary:
|
||||
return {"summary": diary.ai_summary}
|
||||
|
||||
# Generate new summary
|
||||
summary = await generate_summary(activity, rider_ftp=rider.ftp)
|
||||
|
||||
if diary:
|
||||
diary.ai_summary = summary
|
||||
else:
|
||||
diary = DiaryEntry(activity_id=activity_id, ai_summary=summary)
|
||||
session.add(diary)
|
||||
|
||||
await session.commit()
|
||||
return {"summary": summary}
|
||||
|
||||
|
||||
@router.delete("/{activity_id}")
|
||||
async def delete_activity(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
# Delete related records
|
||||
for model in [DataPoint, Interval, ActivityMetrics, PowerCurve, DiaryEntry]:
|
||||
q = select(model).where(model.activity_id == activity_id)
|
||||
result = await session.execute(q)
|
||||
for row in result.scalars().all():
|
||||
await session.delete(row)
|
||||
|
||||
await session.delete(activity)
|
||||
await session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/{activity_id}/diary")
|
||||
async def get_diary(
|
||||
activity_id: uuid.UUID,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
query = select(DiaryEntry).where(DiaryEntry.activity_id == activity_id)
|
||||
result = await session.execute(query)
|
||||
diary = result.scalar_one_or_none()
|
||||
|
||||
if not diary:
|
||||
return {"rider_notes": None, "mood": None, "rpe": None, "sleep_hours": None}
|
||||
|
||||
return {
|
||||
"rider_notes": diary.rider_notes,
|
||||
"mood": diary.mood,
|
||||
"rpe": diary.rpe,
|
||||
"sleep_hours": diary.sleep_hours,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{activity_id}/diary")
|
||||
async def update_diary(
|
||||
activity_id: uuid.UUID,
|
||||
data: dict,
|
||||
rider: Rider = Depends(get_current_rider),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
activity = await session.get(Activity, activity_id)
|
||||
if not activity or activity.rider_id != rider.id:
|
||||
raise HTTPException(status_code=404, detail="Activity not found")
|
||||
|
||||
query = select(DiaryEntry).where(DiaryEntry.activity_id == activity_id)
|
||||
result = await session.execute(query)
|
||||
diary = result.scalar_one_or_none()
|
||||
|
||||
if not diary:
|
||||
diary = DiaryEntry(activity_id=activity_id)
|
||||
session.add(diary)
|
||||
|
||||
for field in ["rider_notes", "mood", "rpe", "sleep_hours"]:
|
||||
if field in data:
|
||||
setattr(diary, field, data[field])
|
||||
|
||||
await session.commit()
|
||||
return {
|
||||
"rider_notes": diary.rider_notes,
|
||||
"mood": diary.mood,
|
||||
"rpe": diary.rpe,
|
||||
"sleep_hours": diary.sleep_hours,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user