Add per-route top-10 leaderboard to activity detail
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user