bc437cce92
Fixes:
- Dashboard: featured most-recent activity card with map + stats
- Maps default to Street; preferCanvas + larger tile buffer for smoother pan/zoom
- Running cadence as colour-banded dots + 165 spm guide line
- Routes: inline row expansion, rename (PATCH /routes/{id}), podium + deltas, tiled map
- Records: remove reversed pace Y-axis
- Profile: remove resting HR; add goal weight
- Health: snapshot weight carry-forward; VO2 trend axis 30-70;
weight goal line + kg/st-lb toggle + axis max; sleep 8h/avg lines
- Garmin sync progress moved to global store with persistent floating bar
Features:
- Speed-coloured activity route (default) with Speed/Solid toggle
- GPS-geometry segments: draw on map, match across all activities,
1st/2nd/3rd leaderboard + podium badges (replaces old distance segments)
- Lap bests: best time per lap across a route + delta column
- Body Battery: highlight activity time windows
Schema: users.goal_weight_kg ALTER; new segments/segment_efforts tables.
Removes RouteSegment, the Segments page, and segment-bests endpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.user import User, PersonalRecord, NamedRoute, HealthMetric, Activity
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ─── Personal Records ────────────────────────────────────────────────────────
|
|
|
|
class PROut(BaseModel):
|
|
id: int
|
|
sport_type: str
|
|
distance_m: float
|
|
distance_label: str
|
|
duration_s: float
|
|
achieved_at: datetime
|
|
activity_id: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("/", response_model=List[PROut])
|
|
async def list_records(
|
|
sport_type: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
q = select(PersonalRecord).where(
|
|
PersonalRecord.user_id == current_user.id,
|
|
PersonalRecord.is_current_record == True,
|
|
)
|
|
if sport_type:
|
|
q = q.where(PersonalRecord.sport_type == sport_type)
|
|
q = q.order_by(PersonalRecord.distance_m)
|
|
result = await db.execute(q)
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/routes")
|
|
async def route_records(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Fastest activity per named route (course records)."""
|
|
from sqlalchemy import text
|
|
rows = await db.execute(
|
|
text("""
|
|
SELECT DISTINCT ON (nr.id)
|
|
nr.id AS route_id,
|
|
nr.name AS route_name,
|
|
nr.sport_type,
|
|
nr.distance_m,
|
|
nr.reference_polyline,
|
|
a.id AS activity_id,
|
|
a.name AS activity_name,
|
|
a.duration_s,
|
|
a.start_time,
|
|
a.avg_speed_ms
|
|
FROM named_routes nr
|
|
JOIN activities a ON a.named_route_id = nr.id AND a.user_id = nr.user_id
|
|
WHERE nr.user_id = :uid AND a.duration_s IS NOT NULL
|
|
ORDER BY nr.id, a.duration_s ASC
|
|
"""),
|
|
{"uid": current_user.id},
|
|
)
|
|
return [dict(r._mapping) for r in rows]
|
|
|
|
|
|
@router.get("/history/{distance_label}")
|
|
async def record_history(
|
|
distance_label: str,
|
|
sport_type: str = "running",
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Show progression of a PR over time."""
|
|
result = await db.execute(
|
|
select(PersonalRecord).where(
|
|
PersonalRecord.user_id == current_user.id,
|
|
PersonalRecord.sport_type == sport_type,
|
|
PersonalRecord.distance_label == distance_label,
|
|
).order_by(PersonalRecord.achieved_at)
|
|
)
|
|
return result.scalars().all()
|