athlete page first draft
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user