11 KiB
BincioActivity — Cheatsheet
Daily workflow
# Option A — local files (Karoo / Garmin / Wahoo)
uv run bincio extract # processes new/changed files, skips unchanged
# Option B — pull from Strava (incremental; credentials in extract_config.yaml)
uv run bincio import strava # fetches only activities since last sync
# Rebuild the site (merges any sidecar edits, then builds)
uv run bincio render
# Done — copy site/dist/ to your host
Extract
uv run bincio extract # full run using extract_config.yaml
uv run bincio extract --since 2025-01-01 # only files newer than date
uv run bincio extract --file ride.gpx # single file → JSON on stdout
uv run bincio extract --input ~/rides \
--output ~/bincio_data # override config paths
uv run bincio extract --dev 50 # dev mode: 50 files → /tmp/bincio_dev/
Re-extraction is safe — unchanged files are skipped (hash-based dedup).
To force a full re-extract: rm -rf ~/bincio_data && uv run bincio extract
Dev mode
--dev N samples N files evenly across the full file list (spread by date and format)
and writes to /tmp/bincio_dev/ so your real data is never touched. Use it for fast
iteration on UI or pipeline changes:
uv run bincio extract --dev 50
uv run bincio import strava --dev 50 # N most recent Strava activities
uv run bincio render --serve --data-dir /tmp/bincio_dev
Import from Strava
# Install (one-time)
uv sync --extra strava
# Add credentials to extract_config.yaml (gitignored — safe for secrets):
# import:
# strava:
# client_id: 12345
# client_secret: your_secret
# First run — opens browser for OAuth, then imports all activities:
uv run bincio import strava
# Subsequent runs are incremental (only fetches since last sync):
uv run bincio import strava
# Other options:
uv run bincio import strava --since 2025-01-01 # explicit date cutoff
uv run bincio import strava --reauth # force new OAuth flow
uv run bincio import strava --output ~/other_dir # override output dir
uv run bincio import strava --dev 50 # dev mode: 50 most recent → /tmp/bincio_dev/
Credentials resolution order:
--client-id/--client-secretflagsSTRAVA_CLIENT_ID/STRAVA_CLIENT_SECRETenv varsimport.strava.client_id/client_secretinextract_config.yaml
Tokens saved to ~/.config/bincio/strava.json and auto-refreshed (6h TTL).
Sync state (imported IDs + last sync timestamp) in data_dir/_strava_sync.json.
File upload (web UI)
When PUBLIC_EDIT_URL is set in site/.env, a ↑ button appears in the nav.
Drag a FIT/GPX/TCX onto the modal → the activity is extracted and appears immediately.
Render
uv run bincio render # merge edits + production build → site/dist/
uv run bincio render --serve # merge edits + dev server → http://localhost:4321
uv run bincio render --data-dir ~/bincio_data # explicit data dir
bincio render always runs merge_all() first (applies sidecar edits, produces _merged/),
then symlinks site/public/data → _merged/ and runs the Astro build or dev server.
# Direct npm (skips merge step — use for quick site-only iteration)
cd site
npm run dev
npm run build
npm run preview
Edit
# Install edit dependencies (FastAPI + uvicorn) — one-time
uv sync --extra edit
# Start the edit server (port 4041 by default)
uv run bincio edit --data-dir ~/bincio_data
# Set PUBLIC_EDIT_URL=http://localhost:4041 in site/.env to enable the Edit button
# Then browse to any activity and click Edit — a drawer opens in the same page
Saves write a sidecar .md to ~/bincio_data/edits/{id}.md and immediately
trigger a merge. Refresh the page to see the updated content.
Sidecar format
---
title: "Renamed title"
sport: cycling
gear: "Trek Domane"
highlight: true # sort to top of feed
private: false # true = hidden from feed
hide_stats: [cadence] # suppress stat panels
---
Description in **markdown**. Images go in the gallery — drag & drop in the Edit drawer.
Python / tests
uv sync # install / update deps
uv run bincio --help # CLI reference
uv run pytest # full test suite
uv run pytest tests/test_fit.py -x # single file, stop on first fail
uv run pytest -k "sport" # run tests matching keyword
uv run pytest -v # verbose output
Data store layout
~/bincio_data/
index.json ← feed index (all activities, summaries)
activities/
2024-05-15T08:30:00Z.json ← full detail + 1Hz timeseries
2024-05-15T08:30:00Z.geojson ← simplified GPS track
Activity ID format: YYYY-MM-DDTHH:MM:SSZ (UTC, always Z suffix).
IDs are stable — safe to use in bookmarks and links.
extract_config.yaml — key fields
This file is gitignored — copy from extract_config.example.yaml and add your credentials safely.
owner:
handle: yourname
display_name: Your Name
input:
dirs:
- ~/Activities # scanned recursively for GPX/FIT/TCX/.gz
metadata_csv: ~/strava_export/activities.csv # optional
output:
dir: ~/bincio_data
default_privacy: public # public | blur_start | no_gps | private
incremental: true # false = re-process everything
track:
rdp_epsilon: 0.0001 # GPS simplification — larger = fewer points
timeseries_hz: 1 # samples/sec in stored JSON (1 = 1 Hz)
import:
strava:
client_id: 12345 # from strava.com/settings/api
client_secret: abc # Authorization Callback Domain must be: localhost
athlete:
max_hr: 182 # used for context; zones below are authoritative
ftp_w: 280 # functional threshold power in watts
hr_zones: # 5-zone Coggan, explicit bpm boundaries [[lo, hi], ...]
- [0, 115] # Z1 recovery
- [115, 137] # Z2 endurance
- [137, 155] # Z3 tempo
- [155, 169] # Z4 threshold
- [169, 999] # Z5 VO2max
power_zones: # 7-zone Coggan, explicit watt boundaries
- [0, 168] # Z1 active recovery (< 55% FTP)
- [168, 224] # Z2 endurance (55–75%)
- [224, 266] # Z3 tempo (75–90%)
- [266, 308] # Z4 threshold (90–105%)
- [308, 364] # Z5 VO2max (105–120%)
- [364, 420] # Z6 anaerobic (120–150%)
- [420, 9999] # Z7 neuromuscular (> 150%)
Zones are written into index.json under owner.athlete at extract time and
displayed as overlays on HR and Power histograms in the activity detail page.
After changing zones, re-run uv run bincio extract to update index.json.
Privacy
| Value | Track served | Stats | In index |
|---|---|---|---|
public |
Full GPS | ✓ | ✓ |
blur_start |
First/last 200 m removed | ✓ | ✓ |
no_gps |
None | ✓ | ✓ |
private |
None | ✗ | ✗ |
Set per-activity in a sidecar .md file, or globally via default_privacy.
Sports
Canonical sport values: cycling running hiking walking swimming skiing other
Sub-sports: road mountain gravel indoor trail track nordic
FIT files: sport is read from the sport frame, with session frame as fallback.
Strava CSV: Activity Type column overrides the FIT-detected sport (authoritative).
Mapping lives in bincio/extract/sport.py.
Patching activities (manual fixes)
Prefer the Edit drawer for title/sport/description/photo changes — it writes a sidecar and keeps extracted data pristine. For bulk fixes or fields not exposed in the UI, patch the JSON directly:
# Fix sport for a single activity
python3 -c "
import json
p = 'site/public/data/activities/2025-03-16T113005Z.json'
d = json.load(open(p))
d['sport'] = 'skiing'
d['sub_sport'] = 'nordic'
json.dump(d, open(p,'w'), separators=(',',':'))
"
# Then update the index.json to match
python3 -c "
import json
idx = json.load(open('site/public/data/index.json'))
for a in idx['activities']:
if a['id'] == '2025-03-16T113005Z':
a['sport'] = 'skiing'
a['sub_sport'] = 'nordic'
json.dump(idx, open('site/public/data/index.json','w'), separators=(',',':'))
"
Common diagnostics
# Count activities by sport in the data store
python3 -c "
import json, glob
from collections import Counter
files = glob.glob('site/public/data/activities/*.json')
c = Counter(json.load(open(f))['sport'] for f in files)
print(dict(c.most_common()))
"
# Find activities with 0 distance
python3 -c "
import json, glob
for f in glob.glob('site/public/data/activities/*.json'):
d = json.load(open(f))
if (d.get('distance_m') or 0) == 0 and d.get('sport') != 'other':
print(d['id'], d['sport'], d['title'])
"
# Find activities still tagged 'other'
python3 -c "
import json
idx = json.load(open('site/public/data/index.json'))
others = [a for a in idx['activities'] if a['sport'] == 'other']
for a in others[:20]:
print(a['started_at'][:10], a.get('source','?'), a['title'])
print(len(others), 'total')
"
Key files
| File | Purpose |
|---|---|
extract_config.yaml |
Main config — input dirs, output dir, athlete zones, Strava credentials. Gitignored. Copy from .example. |
site/.env |
Site env vars (BINCIO_DATA_DIR, PUBLIC_EDIT_URL) — copy from site/.env.example. Gitignored. |
SCHEMA.md |
BAS format specification |
CLAUDE.md |
Dev notes, gotchas, design decisions |
bincio/render/merge.py |
Sidecar overlay logic — parse_sidecar, merge_all |
bincio/edit/server.py |
FastAPI edit API — GET/POST activity, image upload, file upload (POST /api/upload) |
bincio/import_/strava.py |
Strava OAuth2 client + stream → BAS conversion |
bincio/extract/sport.py |
Sport name normalisation + mapping |
bincio/extract/metrics.py |
Distance, speed, HR, elevation computation |
bincio/extract/parsers/fit.py |
FIT file parser |
site/src/components/ActivityFeed.svelte |
Feed page — card grid + sport filter |
site/src/components/StatsView.svelte |
Stats page — heatmap + year totals |
site/src/components/ActivityMap.svelte |
MapLibre GL map |
site/src/components/ActivityCharts.svelte |
Observable Plot charts |
site/src/lib/format.ts |
formatDistance, formatDuration, sport icons/colors |
site/src/lib/types.ts |
TypeScript types mirroring BAS schema |
site/astro.config.mjs |
Astro + Vite config (MapLibre GL workarounds) |