This commit is contained in:
xds
2026-03-16 14:46:20 +03:00
parent 00db55720c
commit de8c2472e2
45 changed files with 3714 additions and 140 deletions

View File

@@ -1,52 +1,146 @@
import uuid
from datetime import date, timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import get_current_rider
from backend.app.core.database import get_session
from backend.app.models.activity import Activity, ActivityMetrics
from backend.app.models.fitness import PowerCurve
from backend.app.models.rider import Rider
from backend.app.schemas.rider import RiderCreate, RiderUpdate, RiderResponse
from backend.app.schemas.rider import RiderUpdate, RiderResponse, FitnessHistoryResponse
from backend.app.services.fitness import get_fitness_history
router = APIRouter()
@router.post("/profile", response_model=RiderResponse)
async def create_rider(
data: RiderCreate,
session: AsyncSession = Depends(get_session),
):
rider = Rider(**data.model_dump())
session.add(rider)
await session.commit()
await session.refresh(rider)
@router.get("/profile", response_model=RiderResponse)
async def get_rider(rider: Rider = Depends(get_current_rider)):
return rider
@router.get("/profile/{rider_id}", response_model=RiderResponse)
async def get_rider(
rider_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
):
rider = await session.get(Rider, rider_id)
if not rider:
raise HTTPException(status_code=404, detail="Rider not found")
return rider
@router.put("/profile/{rider_id}", response_model=RiderResponse)
@router.put("/profile", response_model=RiderResponse)
async def update_rider(
rider_id: uuid.UUID,
data: RiderUpdate,
rider: Rider = Depends(get_current_rider),
session: AsyncSession = Depends(get_session),
):
rider = await session.get(Rider, rider_id)
if not rider:
raise HTTPException(status_code=404, detail="Rider not found")
update_data = data.model_dump(exclude_unset=True)
ftp_changed = "ftp" in update_data and update_data["ftp"] != rider.ftp
for key, value in update_data.items():
setattr(rider, key, value)
# Recalculate TSS/IF for all activities when FTP changes
if ftp_changed and rider.ftp:
query = (
select(ActivityMetrics)
.join(Activity, Activity.id == ActivityMetrics.activity_id)
.where(Activity.rider_id == rider.id)
.where(ActivityMetrics.normalized_power.isnot(None))
)
result = await session.execute(query)
for metrics in result.scalars().all():
np_val = metrics.normalized_power
duration_q = await session.execute(
select(Activity.duration).where(Activity.id == metrics.activity_id)
)
duration = duration_q.scalar()
if np_val and duration:
metrics.intensity_factor = round(np_val / rider.ftp, 2)
metrics.tss = round(
(duration * np_val * (np_val / rider.ftp))
/ (rider.ftp * 3600)
* 100,
1,
)
await session.commit()
await session.refresh(rider)
return rider
@router.get("/fitness", response_model=list[FitnessHistoryResponse])
async def get_fitness(
days: int = 90,
rider: Rider = Depends(get_current_rider),
session: AsyncSession = Depends(get_session),
):
entries = await get_fitness_history(rider.id, session, days=days)
return entries
@router.get("/weekly-stats")
async def get_weekly_stats(
weeks: int = 8,
rider: Rider = Depends(get_current_rider),
session: AsyncSession = Depends(get_session),
):
today = date.today()
# Start from Monday of current week
start_of_week = today - timedelta(days=today.weekday())
start_date = start_of_week - timedelta(weeks=weeks - 1)
week_col = func.date_trunc('week', Activity.date).label("week")
query = (
select(
week_col,
func.count(Activity.id).label("rides"),
func.sum(Activity.duration).label("duration"),
func.sum(Activity.distance).label("distance"),
func.sum(ActivityMetrics.tss).label("tss"),
)
.select_from(Activity)
.outerjoin(ActivityMetrics, ActivityMetrics.activity_id == Activity.id)
.where(Activity.rider_id == rider.id)
.where(Activity.date >= start_date)
.group_by(week_col)
.order_by(week_col)
)
result = await session.execute(query)
return [
{
"week": row.week.strftime("%Y-%m-%d") if row.week else None,
"rides": row.rides,
"duration": row.duration or 0,
"distance": round(float(row.distance or 0) / 1000, 1),
"tss": round(float(row.tss or 0), 0),
}
for row in result
]
@router.get("/personal-records")
async def get_personal_records(
rider: Rider = Depends(get_current_rider),
session: AsyncSession = Depends(get_session),
):
"""Best power at each standard duration across all activities."""
query = (
select(PowerCurve.curve_data, Activity.date, Activity.name, Activity.id)
.join(Activity, Activity.id == PowerCurve.activity_id)
.where(Activity.rider_id == rider.id)
)
result = await session.execute(query)
# Merge all curves: keep best power + source activity for each duration
records: dict[int, dict] = {}
for row in result:
curve_data = row.curve_data
for dur_str, power in curve_data.items():
dur = int(dur_str)
if dur not in records or power > records[dur]["power"]:
records[dur] = {
"duration": dur,
"power": power,
"activity_id": str(row.id),
"activity_name": row.name or "Ride",
"date": row.date.isoformat(),
}
# Sort by duration
return sorted(records.values(), key=lambda r: r["duration"])