Files
bincio-activity/CHEATSHEET.md
T
2026-03-29 22:06:22 +02:00

268 lines
8.2 KiB
Markdown
Raw 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
# 1. Drop new .fit / .gpx / .tcx files into your input dir, then:
uv run bincio extract
# 2. Rebuild the site (merges any sidecar edits, then builds)
uv run bincio render
# 3. 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`
---
## 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
```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)
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, privacy) |
| `site/.env` | Site env vars (`BINCIO_DATA_DIR`, `PUBLIC_EDIT_URL`) — copy from `.env.example` |
| `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 |
| `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) |