Files
bincio-activity/CHANGELOG.md
T
2026-03-30 11:43:13 +02:00

8.2 KiB

Changelog

[Unreleased] — 2026-03-30

Athlete page

  • /athlete page — 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.yaml under athlete.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 edit athlete API (GET /api/athlete, POST /api/athlete) — reads/writes edits/athlete.yaml
  • AthleteDrawer.svelte — slide-in profile editor (gated behind PUBLIC_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/ by merge_all()

Extraction pipeline

  • MMP computationcompute_mmp() added to metrics.py; stored in both detail JSON and index summary (enables client-side season filtering without extra fetches)
  • Best-effort computationcompute_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 into athlete.json

Scripts

  • scripts/backfill.py — backfills mmp, best_efforts, and best_climb_m into 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 via initializer= rather than once per task
  • TCX parser fixes — handles both http:// and https:// Garmin namespace URIs
  • Sport classification overhaul
    • FIT parser now reads sport from the session frame as fallback when no separate sport frame is present (fixes Karoo and Strava-generated FIT files)
    • Strava CSV Activity Type used 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"
  • Distance calculation fix — when a FIT device records distance = 0.0 (not null), 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_csv is 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 (not exclude) fixes silent tile worker failure
    • build.target: 'es2022' required for MapLibre's ES2022 class field syntax
    • MapLibre v5 requires explicit center/zoom in Map constructor and setLngLat() before addTo()
  • 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"
  • 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_zones in extract_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
    • $: cellColors precomputed as a reactive Map<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 of toISOString() 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 edit write API — FastAPI server (--data-dir, default port 4041)
    • GET /api/activity/{id} — current values with sidecar overrides applied
    • POST /api/activity/{id} — writes sidecar .md, triggers merge_all()
    • POST /api/activity/{id}/images — multipart image upload
    • DELETE /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
  • 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_URL env 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
  • 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.yaml cleaned up — personal paths removed, metadata_csv commented out with explanation

Infrastructure

  • publish.sh — builds and pushes static site to GitHub Pages via orphan branch