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.
New pref download_disabled_default (stored in user_prefs + mirrored to
_user_settings.json for the render pipeline). When true, apply_sidecar
marks all activities as download_disabled unless the sidecar explicitly
sets download_disabled: false (per-activity opt-in from the edit drawer).
Settings page gets an "Activity defaults" card with the toggle.
New /api/admin/garmin-sync (GET) and /api/admin/garmin-sync/run (POST)
endpoints mirror the Strava equivalents, reading _garmin_sync_status.json
per user and exposing a run-now button. Admin page shows the Garmin table
below the Strava one, with auth_error/api_error/ok badges and live polling
while a sync is running.
GET /api/me/sync-status reads _strava_sync_status.json and
_garmin_sync_status.json for the logged-in user. On page load the nav
script checks this endpoint and, if either service has status=auth_error,
turns the upload arrow orange with a tooltip naming the disconnected
service(s).
New endpoint: GET /api/activity/{id}/download/{bas|original|gpx}
- bas: streams the BAS detail JSON as an attachment
- original: streams the original FIT or GPX file from originals/
- gpx: generates a GPX from the timeseries (always available when GPS exists)
download_disabled flag stored in sidecar (edits/{id}.md), propagated to
the merged BAS detail JSON. When set, only the owner can download.
Backend: ops.py writes flag to sidecar; merge.py propagates it to detail
JSON; download.py implements the endpoint; server.py registers the router.
Frontend: EditDrawer gets a "No download" toggle button; ActivityDetail
shows a Download section (hidden when disabled and viewer is not the owner).
feed.json is now a BAS shard index pointing to feed-YYYY-MM.json files
(~150 activities / ~25 KB gzip each) instead of 400+ sequential feed-N.json
pages. The frontend can now jump directly to a specific month when filtering
by year or date range, without loading all newer data first.
- merge.py: write_combined_feed groups by YYYY-MM and emits a shard index
- dataloader.ts: isYearShardUrl matches feed-YYYY-MM.json; loadCombinedFeed
returns pendingShards; FeedPage interface and loadCombinedFeedPage removed
- ActivityFeed.svelte: _yearFromShard handles both index-YYYY and feed-YYYY-MM;
feedNextPage/feedTotalPages/loadingAllFeedPages removed; infinite-loop bug
fixed (toLoad.length guard before setting loadingAllShards); onMount uses
pendingShards from loadCombinedFeed
- Stop fetching combined-feed pages once the oldest activity in a batch predates
the from-date (feed is newest-first, so everything needed is already loaded)
- Show "Loading…" instead of "No activities found" while eager-load is in progress
- Constrain From max to customTo (or today) and To min to customFrom so the
range can't be inverted via the date pickers
Previous attempt used dateFrom (a derived $: variable) as the trigger which
Svelte 5 doesn't reliably track as a dependency of a side-effect $: block.
Replace with the primary let-variables (customFrom, customTo, datePre) that
Svelte does track statically.
Also extend eager-loading to cover the global combined feed (feedNextPage)
so date/search filtering works on multi-user instances too, not just per-user
profile pages (pendingShards).
The initial page load only fetches the most recent year shard. Selecting a
date range or year preset that spans an older shard returned no results because
those shards were never loaded. Extend the existing search eager-load trigger
to also fire on any non-empty dateFrom, covering both custom date inputs and
year preset buttons.
Status cycles open → awaiting → done → reopen.
Awaiting ideas float to the top in a 'Waiting for your feedback' section
with an amber border (#f59e0b).
Admin can attach an implementation note to any awaiting idea via
POST /api/ideas/{id}/comment. The note appears inside the same card
in a distinct sub-box with a subtle amber tint border, editable inline.
The sub-box is visible to all users once a note exists.
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.
- _is_outdoor now also excludes activities whose title matches /\bzwift\b/i,
covering the ~50 Strava-imported Zwift rides that lack sub_sport metadata.
- EditDrawer waits 900ms after a successful save before dispatching 'saved'
(which closes the drawer), so the green "Saved" confirmation is visible.
bake_tracks now writes tracks_YYYY.json shards + tracks_index.json manifest
instead of a single monolithic tracks.json. API /api/me/tracks returns the
manifest; /api/me/tracks/{year} serves individual shards. Explore.svelte
fetches the two most recent years eagerly then streams the rest in the
background so the map renders immediately with recent data.
- 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