diff --git a/CHEATSHEET.md b/CHEATSHEET.md new file mode 100644 index 0000000..86f8b5e --- /dev/null +++ b/CHEATSHEET.md @@ -0,0 +1,208 @@ +# BincioActivity — Cheatsheet + +## Daily workflow + +```bash +# 1. Drop new .fit / .gpx / .tcx files into your input dir, then: +bincio extract + +# 2. Rebuild the site +cd site && npm run build + +# 3. Done — copy site/dist/ to your host +``` + +--- + +## Extract + +```bash +bincio extract # full run using extract_config.yaml +bincio extract --since 2025-01-01 # only files newer than date +bincio extract --file ride.gpx # single file → JSON on stdout +bincio extract --input ~/rides \ + --output ~/bincio_data # override config paths +``` + +Re-extraction is safe — unchanged files are skipped (hash-based dedup). +To force a full re-extract: `rm -rf ~/bincio_data && bincio extract` + +--- + +## Site + +```bash +cd site + +# Symlink data (do once) +ln -sf ~/bincio_data public/data + +# Dev server with hot reload +npm run dev # → http://localhost:4321 + +# Production build +npm run build # → site/dist/ + +# Preview production build locally +npm run preview +``` + +--- + +## Python / tests + +```bash +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 + +```yaml +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) +``` + +--- + +## 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) + +When re-running extract isn't practical, patch the JSON directly: + +```bash +# 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 + +```bash +# 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, privacy) | +| `SCHEMA.md` | BAS format specification | +| `CLAUDE.md` | Dev notes, gotchas, design decisions | +| `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) |