Files
bincio-activity/CHEATSHEET.md
2026-03-30 20:09:01 +02:00

334 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BincioActivity — Cheatsheet
## Daily workflow
```bash
# 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
```bash
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:
```bash
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
```bash
# 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:
1. `--client-id` / `--client-secret` flags
2. `STRAVA_CLIENT_ID` / `STRAVA_CLIENT_SECRET` env vars
3. `import.strava.client_id` / `client_secret` in `extract_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
```bash
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.
```bash
# Direct npm (skips merge step — use for quick site-only iteration)
cd site
npm run dev
npm run build
npm run preview
```
## Edit
```bash
# 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
```markdown
---
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
```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
This file is **gitignored** — copy from `extract_config.example.yaml` and add your credentials safely.
```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)
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 (5575%)
- [224, 266] # Z3 tempo (7590%)
- [266, 308] # Z4 threshold (90105%)
- [308, 364] # Z5 VO2max (105120%)
- [364, 420] # Z6 anaerobic (120150%)
- [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:
```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, 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) |