Find the activity that holds each MMP record by scanning per-activity
mmp arrays. Activity title appears in the chart hover tooltip. A table
below the chart lists every duration with the record watts, activity
title (linked), and date. The table has its own all-time/365d/90d toggle
independent of the chart overlays.
Remove the per-duration VAM curve everywhere (metrics, summaries, detail
JSON, athlete.json, VamChart.svelte, AthleteView VAM tab). Keep only
climbing_vam_mh per activity. Add it to activity summaries so NerdCorner
can plot average climbing VAM per week/month year-over-year alongside
distance/elevation/time. Add --backfill-vam-summary flag to copy the
field from existing detail JSONs into index.json without re-extracting.
Extract pipeline now computes two VAM metrics per activity (cycling,
running, hiking, walking):
- climbing_vam_mh: VAM on ascending segments only, using 30 s forward
lookahead to classify climbing vs. flat/descent (stored in detail JSON)
- vam_curve: [[duration_s, vam_mh], ...] best VAM per standard duration
(60 s – 1 h), sliding window on 30 s smoothed elevation, only windows
with ≥ 10 m net gain count (stored in summary + detail)
Athlete JSON aggregates vam_curve across all activities (all_time,
last_365d, last_90d), same structure as power_curve.
Frontend:
- ActivityDetail shows "Climbing VAM" stat (grouped with elevation)
- AthleteView adds a "VAM Curve" tab that appears only when the athlete
has climbing data; renders VamChart (new component, mirrors MmpChart)
vam_curve stripped from combined global feed; kept in user year shards
for season-based on-the-fly aggregation in VamChart.
Requires bincio reextract to backfill existing activities.
In multi-user instances /api/me is async and usually hasn't returned by the
time onMount runs, leaving isOwner=false. Subscribe to the bincio:me event
(fired by Base.astro when /api/me resolves) so the reactive TABS filter
re-evaluates and Explore / Nerd Corner appear without needing Cmd+Shift+R.
- bincio/explore.py: bake_tracks() simplifies GPS coords (RDP ε=0.0001),
strips to [lng,lat], groups by sport type, writes per-handle tracks.json
- bake-tracks CLI command; render CLI calls _bake_tracks() after each build;
strava_zip runs it once at end of batch
- /api/me/tracks endpoint serves the baked file; wipe_user cleans it up
- Explore.svelte: MapLibre full-screen map with sidebar — type pills,
year/month date filter, Lines / Heatmap (global or by-type) view modes
- AthleteView: Explore tab visible only to profile owner (checks __bincioMe)
- Base.astro: fullscreen prop + Planner nav link
- best_activity_id now included in segment_summary API response
- Best time is a direct link to the activity that produced it
- Clicking a row expands an inline effort list (lazy-loaded from
/api/segments/{id}/efforts): date linked to activity, time, Δ vs PR
- Clicking again collapses; ▲/▼ chevron shows state
AthleteView: use segmentsFetched flag to prevent infinite fetch loop when
there are no efforts (segmentSummary.length === 0 was re-triggering the
reactive statement after every empty response). Also improve empty state
message and reset flag after rescan so the table reloads.
astro.config.mjs: extend shell fallback plugin to cover /segments/{id}/
the same way /activity/{id}/ is handled, so segment detail pages work in
the dev server without nginx.
New API endpoints:
- GET /api/segments/{id} — single segment metadata
- GET /api/activities/{id}/segment_efforts — efforts for an activity (auth)
- GET /api/users/{handle}/segment_summary — public best time + count per segment
New components:
- SegmentDetail.svelte — map + metadata + effort table (with PR/Δ) + rescan button
- SegmentsPage.svelte — URL router: shows detail when /segments/{id}/, list otherwise
Updated:
- segments/index.astro — now uses SegmentsPage router
- nginx-activity.conf — add /segments/ try_files rule for client-side routing
- ActivityDetail.svelte — segment efforts block below laps
- AthleteView.svelte — Segments tab with best time + effort count per segment
- format.ts — add formatElapsed() for compact m:ss display
- "unlisted" = not shown in the public feed, but GPS track, timeseries
and detail JSON are all accessible by direct URL (security by obscurity)
- "private" accepted as legacy alias everywhere (backward compat with
existing data on disk)
- New writes from Strava sync / ZIP upload / sidecar use "unlisted"
- Only "no_gps" now suppresses the GPS track
- isUnlisted() helper in format.ts used by all Svelte/Astro components
- SCHEMA.md and CLAUDE.md document the privacy model and the distinction
between "unlisted" and "no_gps"