athlete page first draft

This commit is contained in:
Davide Scaini
2026-03-30 09:05:18 +02:00
parent 2a1493a3e5
commit ec6175b143
8 changed files with 594 additions and 3 deletions
+52
View File
@@ -68,6 +68,7 @@ def write_activity(
"bbox": list(metrics.bbox) if metrics.bbox else None,
"start_latlng": list(metrics.start_latlng) if metrics.start_latlng else None,
"end_latlng": list(metrics.end_latlng) if metrics.end_latlng else None,
"mmp": metrics.mmp,
"laps": [_serialise_lap(lap) for lap in activity.laps],
"timeseries": build_timeseries(activity.points, activity.started_at, privacy),
"source": source,
@@ -115,6 +116,7 @@ def build_summary(
"max_hr_bpm": metrics.max_hr_bpm,
"avg_cadence_rpm": metrics.avg_cadence_rpm,
"avg_power_w": metrics.avg_power_w,
"mmp": metrics.mmp,
"source": _infer_source(activity),
"privacy": privacy,
"detail_url": f"activities/{activity_id}.json",
@@ -124,6 +126,56 @@ def build_summary(
}
def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config: dict) -> None:
"""Aggregate per-activity MMP curves into athlete.json.
Computes element-wise max MMP for:
- all_time
- last_365d
- last_90d
The site reads this single file for the athlete/power-curve page.
Per-activity mmp is already in each summary for client-side season filtering.
"""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
def _cutoff_iso(days: int) -> str:
from datetime import timedelta
return (now - timedelta(days=days)).isoformat()
cutoff_365 = _cutoff_iso(365)
cutoff_90 = _cutoff_iso(90)
def _merge_mmps(activity_mmps: list[list[list[int]]]) -> list[list[int]]:
"""Element-wise max across a list of mmp arrays."""
best: dict[int, int] = {}
for mmp in activity_mmps:
for d, w in mmp:
if d not in best or w > best[d]:
best[d] = w
return [[d, w] for d, w in sorted(best.items())]
all_mmps = [s["mmp"] for s in summaries if s.get("mmp")]
mmps_365 = [s["mmp"] for s in summaries if s.get("mmp") and s["started_at"] >= cutoff_365]
mmps_90 = [s["mmp"] for s in summaries if s.get("mmp") and s["started_at"] >= cutoff_90]
athlete = {
"bas_version": "1.0",
"generated_at": now.isoformat(),
"power_curve": {
"all_time": _merge_mmps(all_mmps) if all_mmps else None,
"last_365d": _merge_mmps(mmps_365) if mmps_365 else None,
"last_90d": _merge_mmps(mmps_90) if mmps_90 else None,
},
**athlete_config,
}
(output_dir / "athlete.json").write_text(
json.dumps(athlete, indent=2, ensure_ascii=False)
)
def write_index(summaries: list[dict], output_dir: Path, owner: dict) -> None:
"""Write index.json (sorted newest first)."""
sorted_summaries = sorted(