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
+132 -1
View File
@@ -361,6 +361,134 @@ are served at `data/activities/images/{id}/{filename}` by the Astro dev server.
- **`highlight`**: sorts to top of feed; visual badge TBD
- **Edit UI**: drawer in Astro site, `bincio edit` is a pure write API (no HTML serving)
## Athlete page — design plan
### Goal
A `/athlete` page (and `/athlete/edit` drawer) giving the user:
1. **Performance analytics** — power curve (MMP), best efforts, optionally fitness/freshness
2. **Profile editing** — zones, gear (bikes/shoes), personal data — no YAML editing required
### Mean Maximal Power (MMP) curve
For every duration D, the MMP is the highest average power sustained over any contiguous
D-second window across all activities. Plotted on a log-scale x-axis.
**Key features:**
- **Time range filter**: all-time, last 30/90/365 days, or user-defined seasons
- **Season overlay**: multiple seasons plotted on the same chart for comparison
(e.g. "2023 vs 2024 vs 2025" — this is the primary use case)
- **Durations**: a fixed log-scale set, e.g.:
`1, 2, 5, 10, 15, 20, 30, 60, 120, 180, 300, 600, 1200, 1800, 3600` seconds
- **Null handling**: if an activity is shorter than duration D, it contributes nothing
to that point. No interpolation. The curve simply ends where data runs out.
- **Modelled curve overlay** (future): 2-parameter Critical Power model fitted to
the data; shows predicted W for any duration, even beyond recorded efforts.
**Where to compute:**
At **extract time**, each activity gets an `mmp` array:
```json
"mmp": [[1, 850], [5, 720], [30, 580], [300, 340], [3600, 210]]
```
Each pair is `[duration_s, avg_watts]`. Only activities with power data get this field.
The site then takes the **element-wise max** across all activities (filtered by date range).
This keeps the site fully static — no server needed to render the curve.
Computing MMP per activity is O(n × D) where n = timeseries length, D = number of
duration points (~15). At 1 Hz, a 2-hour ride is 7200 points × 15 durations = trivial.
Use a sliding window approach: for each duration d, maintain a running sum and advance
the window one sample at a time.
**Season definition** (user-configurable):
```yaml
athlete:
seasons:
- name: "2025"
start: "2025-01-01"
end: "2025-12-31"
- name: "2024"
start: "2024-01-01"
end: "2024-12-31"
```
If no seasons defined, the UI offers fixed presets (last 30d / 90d / 365d / all-time).
### Athlete profile editing — reusing edit infrastructure
Same pattern as activity editing:
```
bincio edit --data-dir ~/bincio_data # same server, new endpoints
```
New API endpoints:
- `GET /api/athlete` — current athlete config (zones, gear, display name)
- `POST /api/athlete` — write `edits/athlete.yaml`, trigger `merge_all()`
`edits/athlete.yaml` format:
```yaml
display_name: "Davide"
handle: "brutsalvadi"
max_hr: 190
ftp_w: 210
hr_zones:
- [0, 104]
- [104, 142]
- [142, 165]
- [165, 176]
- [176, 999]
power_zones:
- [0, 115]
# ...
gear:
bikes:
- name: "Trek Domane"
type: cycling
notes: "Road endurance"
shoes:
- name: "Asics GT-2000"
type: running
seasons:
- name: "2025"
start: "2025-01-01"
end: "2025-12-31"
```
The server reads `extract_config.yaml` as base defaults, applies `edits/athlete.yaml`
overrides on top, and writes back to `edits/athlete.yaml` on POST. The `extract_config.yaml`
is never written by the server — it stays as the authoritative static config.
`merge_all()` also writes athlete data into `_merged/athlete.json` which the site reads.
### AthleteDrawer.svelte (profile editing)
Reuses the same drawer pattern as `EditDrawer.svelte`:
- Number inputs for `max_hr`, `ftp_w`
- Zone editor: table of rows `[lo, hi]` with + / buttons; auto-fills `lo` from previous `hi`
- Gear list: add/remove bikes and shoes; name + type + notes fields
- Season list: add/remove date ranges with names
### Site page: `/athlete`
Two tabs or sections:
1. **Performance** — MMP curve chart (Observable Plot, log x-axis), date range selector
2. **Profile** — display of current zones, gear list; Edit button opens AthleteDrawer
The MMP chart uses `index.json`'s `activities` array (already loaded by the feed) — filter
to power-having activities, pull their `mmp` arrays, take element-wise max per season.
### Implementation order
1. Add `mmp` computation to `metrics.py` and writer
2. Add `mmp` field to BAS schema and `types.ts`
3. Add `/api/athlete` GET+POST to the edit server
4. `merge_all()` writes `_merged/athlete.json`
5. Astro page `site/src/pages/athlete/index.astro`
6. `MmpChart.svelte` — Observable Plot line, log-scale x, multi-season overlay
7. `AthleteDrawer.svelte` — zones + gear editing form
8. Season config in `extract_config.yaml` / `edits/athlete.yaml`
## Known issues / next steps
- `bincio render` Python CLI is a stub — site is built via `npm run build` directly
@@ -377,7 +505,10 @@ are served at `data/activities/images/{id}/{filename}` by the Astro dev server.
- [ ] `bincio render` Python CLI wraps `astro build` properly
- [ ] Friends/federation pages in site
- [ ] Personal records page
- [ ] Athlete page: MMP power curve with season overlay
- [ ] Athlete page: profile editor (zones, gear, seasons) via AthleteDrawer
- [ ] MMP computation at extract time → `mmp` field in BAS JSON
- [ ] Personal records page (best efforts: 5km, 10km, etc.)
- [ ] Activity search / full-text filter in feed
- [ ] Map thumbnail in activity cards (SVG path from GeoJSON)
- [ ] GitHub Actions template for auto-publish