10 KiB
10 KiB
Changelog
[Unreleased] — 2026-03-30
Data ingestion
-
bincio import strava— OAuth2 Strava importer (bincio/import_/strava.py+bincio/import_/cli.py)- One-shot local OAuth2 callback server (port 8976); opens browser, receives code, exchanges for tokens
- Tokens saved to
~/.config/bincio/strava.json; auto-refreshed on expiry (6h TTL) - Fetches paginated activity list with
after=timestamp for efficient incremental runs - Per activity:
GET /activities/{id}/streams→_strava_to_parsed()→compute()→write_activity() _patch_from_summary(): fillsNonemetrics from Strava summary when sensors are missing (manual entries, indoor rides)- Sync state persisted in
data_dir/_strava_sync.json(imported IDs + last sync timestamp) - Rate limit tracking via
X-RateLimit-Usage; warns at 85% of 15-min window; auto-retries on 429 - Credentials read from (in order): CLI flags → env vars →
extract_config.yamlunderimport.strava - Install:
uv sync --extra strava
-
Web file upload —
POST /api/uploadinbincio/edit/server.py- Accepts FIT/GPX/TCX (
.gzvariants too); 409 if activity already exists - Runs full extract pipeline inline:
parse_file()→compute()→write_activity()→merge_all() - Staged to
data_dir/_uploads/during processing; cleaned up infinally ↑button in site nav, gated behindPUBLIC_EDIT_URL; drag-and-drop modal; auto-redirects on success
- Accepts FIT/GPX/TCX (
-
extract_config.yamlis now gitignored — safe to store credentials underimport.stravaStravaConfigdataclass added tobincio/extract/config.py; parsed fromimport.strava:blockextract_config.example.yamlis the tracked template
-
Theme-aware heatmap (
StatsView.svelte) —applyIntensity()now lerps from the correct background colour in both dark (zinc-800#27272a) and light (zinc-200#e4e4e7) modes;emptyColorandbaseRgbreactive todata-themeviaMutationObserver
Athlete page
/athletepage — three-tab layout: Power Curve · Records · Profile- Mean Maximal Power (MMP) curve — computed at extract time for each activity with power data
- Sliding-window O(n) algorithm over 1 Hz power timeseries; 15 standard durations (1 s → 1 h)
- Multi-curve overlay with range selector: All time / Last 365 d / Last 90 d / user-defined seasons
- Log-scale x-axis via Observable Plot; FTP reference line; per-point tooltips
- Seasons configurable in
extract_config.yamlunderathlete.seasons
- Personal records (Records tab) — sport-specific best efforts computed via sliding window
- Running: 400 m, 1 km, 1 mile, 5 km, 10 km, half marathon, marathon
- Cycling: 5 km, 10 km, 20 km, 50 km, 100 km
- Swimming: 100 m, 200 m, 500 m, 1 km, 2 km
- Table shows time, pace (running) or speed (cycling/swimming), date, activity link
- Hiking / Walking: longest distance and most elevation gain
- Best climbs — top 10 biggest single climbs (Kadane's algorithm on 1 Hz elevation deltas); ranked table with elevation, date, activity link
- Profile tab — max HR, FTP, HR zones, power zones
bincio editathlete API (GET /api/athlete,POST /api/athlete) — reads/writesedits/athlete.yamlAthleteDrawer.svelte— slide-in profile editor (gated behindPUBLIC_EDIT_URL)- Max HR and FTP number inputs
- HR and power zone tables: changing a zone's upper bound auto-cascades to the next zone's lower bound
- Season list: name + date range, add/remove rows
athlete.json— written at extract time; contains pre-aggregated MMP curves and records; symlinked into_merged/bymerge_all()
Extraction pipeline
- MMP computation —
compute_mmp()added tometrics.py; stored in both detail JSON and index summary (enables client-side season filtering without extra fetches) - Best-effort computation —
compute_best_efforts()two-pointer sliding window on 1 Hz speed;_best_climb()Kadane's on elevation deltas write_athlete_json()— aggregates MMP and records from all summaries intoathlete.json
Scripts
scripts/backfill.py— backfillsmmp,best_efforts, andbest_climb_minto existing activity JSONs from already-extracted 1 Hz timeseries; no FIT re-parsing needed (~20 s for 2500 activities)
[0.1.0] — 2026-03-29
Extraction pipeline
- Parallel extraction — activities now processed with
ProcessPoolExecutor; large shared state (Strava lookup, known hashes) sent once per worker viainitializer=rather than once per task - TCX parser fixes — handles both
http://andhttps://Garmin namespace URIs - Sport classification overhaul
- FIT parser now reads sport from the
sessionframe as fallback when no separatesportframe is present (fixes Karoo and Strava-generated FIT files) - Strava CSV
Activity Typeused as authoritative override when present - Expanded sport mapping: e-bike variants (
ebikeride,e_bike_ride),ride,run, date-prefix stripping, and more - Skiing added as first-class sport:
cycling|running|hiking|walking|swimming|skiing|other - Nordic sub-sport: FIT sub_sport values
cross_country_skiing,nordic_skiing,skate_skiing,backcountry_skiing→"nordic"
- FIT parser now reads sport from the
- Distance calculation fix — when a FIT device records
distance = 0.0(notnull), the extractor now falls back to haversine-computed GPS distance instead of using the zero value directly; fixes skiing activities that had valid tracks and speeds but showed 0 km metadata_csvis fully optional — omitting it from config works cleanly; only needed for Strava bulk exports
Site — maps & charts
- MapLibre GL map fully working on the activity detail page
- Static import +
optimizeDeps.include(notexclude) fixes silent tile worker failure build.target: 'es2022'required for MapLibre's ES2022 class field syntax- MapLibre v5 requires explicit
center/zoomin Map constructor andsetLngLat()beforeaddTo()
- Static import +
- Observable Plot charts (elevation, speed, HR, cadence) working
- Switched from dynamic
await import()to static import — fixes unreliable Svelte reactivity - Curve name is
"monotone-x"not"monotoneX"
- Switched from dynamic
- Power chart added as fifth tab alongside elevation/speed/HR/cadence
- HR and power zone histograms — configurable zone boundaries via
athlete.hr_zones/athlete.power_zonesinextract_config.yaml; histogram x-axis capped at actual data max so sentinel values (999,9999) don't stretch the axis - Adjustable trim range on histograms
Site — activity feed
- SVG track thumbnails on feed cards — drawn from
preview_coords(no extra fetch) - Sport filter bar — pill buttons for All / Cycling / Running / Hiking / Walking / Swimming / Skiing / Other
Site — stats page
- Sport filter bar — same pill UI as the feed; all stats and heatmap reflect the selected sport
- Heatmap colour improvements
- Blended colours in "All" mode: each cell's RGB is a weighted average of sport colours by distance
- Percentile-based intensity scaling (active): each day ranked against all active days, spreading colour evenly regardless of km outliers; configurable back to linear/max-relative (documented in CLAUDE.md)
applyIntensity()lerps from zinc-800 background to full sport colour — dim cells fade into the background rather than going black$: cellColorsprecomputed as a reactiveMap<string, string>— fixes Svelte not re-rendering cells when filter changes
- Month label fix — labels embedded in the week-column flex grid (no more absolute-positioning bugs);
getWeeks()uses local date formatting (localISO()) instead oftoISOString()to avoid UTC/local mismatch that produced a spurious "Dec" label at column 0 - Cell tooltips — hovering a cell shows a floating card with date, and for each activity: name, sport, distance, duration; each activity is a clickable link to its detail page; 120 ms grace period when moving from cell to tooltip
Site — activity editing (bincio edit)
bincio editwrite API — FastAPI server (--data-dir, default port 4041)GET /api/activity/{id}— current values with sidecar overrides appliedPOST /api/activity/{id}— writes sidecar.md, triggersmerge_all()POST /api/activity/{id}/images— multipart image uploadDELETE /api/activity/{id}/images/{filename}
- Activity sidecar system (
bincio/render/merge.py)- Sidecars live in
edits/alongside extracted data (never co-mingled with immutable BAS JSON) - Fields:
title,sport,description,hide_stats,highlight,private,gear merge_all()produces_merged/output;public/data→_merged/at runtime
- Sidecars live in
EditDrawer.svelte— slide-in drawer in the Astro site (no separate HTML from the server)- Opens in-page via Edit button; only rendered when
PUBLIC_EDIT_URLenv var is set - Title, sport dropdown, gear, markdown description textarea
- Image drag-and-drop with chip list + delete
- Hide-stats toggle buttons (elevation, speed, heart_rate, cadence, power)
- Highlight and private flags
- Optimistic local update on save — title and description update immediately without reload
- Opens in-page via Edit button; only rendered when
- Photo gallery + lightbox on activity detail page — keyboard navigation (←/→/Esc), filename + counter overlay
- Markdown descriptions rendered with
marked; local relative images suppressed from inline rendering (shown in gallery instead)
Documentation
- README rewritten — philosophy statement front and centre, clear two-stage architecture diagram, quick start
- CHEATSHEET.md added — daily workflow, all CLI commands, config reference, privacy table, patching snippets, diagnostic scripts, key files table
- CLAUDE.md updated — MapLibre GL v5 gotchas, Observable Plot curve names, heatmap colour scaling approaches (linear vs percentile), sidecar/edit architecture decisions
extract_config.example.yamlcleaned up — personal paths removed,metadata_csvcommented out with explanation
Infrastructure
publish.sh— builds and pushes static site to GitHub Pages via orphan branch