adding a cheatsheet
This commit is contained in:
+208
@@ -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) |
|
||||||
Reference in New Issue
Block a user