320 lines
10 KiB
Markdown
320 lines
10 KiB
Markdown
# 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
|
||
```
|
||
|
||
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`
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
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 (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:
|
||
|
||
```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) |
|