Add per-route top-10 leaderboard to activity detail
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 9s

New /activities/{id}/route-leaderboard endpoint ranks the user's timed
efforts on the same route; frontend RouteLeaderboard card sits beside
Laps, showing this activity's time/rank/gap and the top 10 (current
effort highlighted green, also surfaced if outside the top 10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:37:37 +01:00
parent bdd5f80c7e
commit d350e9caea
3 changed files with 179 additions and 2 deletions
+63
View File
@@ -229,6 +229,69 @@ async def get_lap_bests(
return {str(lap_number): best for lap_number, best in rows}
@router.get("/{activity_id}/route-leaderboard")
async def get_route_leaderboard(
activity_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Fastest-time leaderboard across all of this user's activities on the same
route. Returns this activity's rank/gap plus the top 10. Null if the activity
has no associated route (or no timed efforts to rank)."""
act = (await db.execute(
select(Activity).where(
Activity.id == activity_id,
Activity.user_id == current_user.id,
)
)).scalar_one_or_none()
if not act:
raise HTTPException(status_code=404, detail="Activity not found")
if not act.named_route_id:
return None
rows = (await db.execute(
select(
Activity.id, Activity.name, Activity.start_time,
Activity.duration_s, Activity.distance_m, Activity.avg_heart_rate,
)
.where(
Activity.named_route_id == act.named_route_id,
Activity.user_id == current_user.id,
Activity.duration_s.isnot(None),
)
.order_by(Activity.duration_s)
)).all()
if not rows:
return None
fastest_s = rows[0].duration_s
entries = []
current = None
for i, r in enumerate(rows):
entry = {
"rank": i + 1,
"activity_id": r.id,
"name": r.name,
"start_time": r.start_time,
"duration_s": r.duration_s,
"distance_m": r.distance_m,
"avg_heart_rate": r.avg_heart_rate,
"gap_s": r.duration_s - fastest_s,
"is_current": r.id == activity_id,
}
if entry["is_current"]:
current = entry
entries.append(entry)
return {
"route_id": act.named_route_id,
"total": len(entries),
"fastest_s": fastest_s,
"current": current,
"top": entries[:10],
}
@router.patch("/{activity_id}/name")
async def rename_activity(
activity_id: int,