add publish infrastructure and update docs for public release
- publish/manifest: explicit file allowlist for public repo - publish/CLAUDE.md: sanitized version (no personal data paths) - publish/extract_config.example.yaml: generic paths and owner - publish.sh: orphan-branch publish script (single squashed commit) - site/.env.example: documents BINCIO_DATA_DIR and PUBLIC_EDIT_URL - README.md: updated pipeline diagram, quick start, project layout - CHEATSHEET.md: added bincio render and bincio edit sections, sidecar format reference, updated daily workflow
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# BincioActivity — Context for Claude
|
||||
|
||||
## What this project is
|
||||
|
||||
BincioActivity is a federated, open-source, self-hosted activity stats platform
|
||||
(think personal Strava). Two-stage pipeline:
|
||||
|
||||
1. **`bincio extract`** (Python): GPX/FIT/TCX → BAS JSON data store
|
||||
2. **`bincio render`** (Astro/Node): BAS data store → static website
|
||||
|
||||
The BAS (BincioActivity Schema) JSON files are the federation protocol.
|
||||
Anyone can publish their data as BAS JSON and others can include it.
|
||||
|
||||
## Key design decisions
|
||||
|
||||
- **No database, no server** — everything is static files
|
||||
- **Python with uv** for the extract stage
|
||||
- **Astro + Svelte + Tailwind + MapLibre GL + Observable Plot** for the site
|
||||
- **Haversine** (not geopy) for distance calculations (10x faster)
|
||||
- **Worker initializer pattern** for ProcessPoolExecutor — large shared data
|
||||
(strava_lookup dict, known_hashes frozenset) is sent once per worker via
|
||||
`initializer=`, not once per task
|
||||
- **BAS activity IDs** always use UTC with Z suffix for URL safety
|
||||
- **TCX files** from Garmin use both `http://` and `https://` namespace URIs —
|
||||
parser handles both
|
||||
|
||||
## Your data
|
||||
|
||||
- Source: `~/your-activity-data/`
|
||||
- `activities/` — Strava export (GPX, FIT, TCX, all with .gz variants)
|
||||
- Any subdirectories with FIT files from Garmin/Karoo devices
|
||||
- `activities.csv` — Strava metadata (names, descriptions, gear)
|
||||
- Extracted output: `~/bincio_data/` (or `/tmp/bincio_test/` for testing)
|
||||
|
||||
Configure input paths in `extract_config.yaml`.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
bincio/ Python package
|
||||
extract/
|
||||
models.py DataPoint, ParsedActivity, LapData
|
||||
parsers/ GPX, FIT, TCX parsers + factory
|
||||
sport.py sport name normalisation
|
||||
metrics.py haversine-based stats computation (single pass)
|
||||
timeseries.py downsample to 1Hz, build BAS timeseries object
|
||||
simplify.py RDP track simplification → GeoJSON
|
||||
dedup.py exact (hash) + near-duplicate detection
|
||||
strava_csv.py Strava activities.csv importer
|
||||
writer.py BAS JSON + GeoJSON writer
|
||||
config.py extract_config.yaml loader
|
||||
cli.py `bincio extract` CLI
|
||||
render/
|
||||
cli.py `bincio render` CLI (symlinks data, runs astro build/dev)
|
||||
merge.py sidecar edit overlay (produces _merged/)
|
||||
edit/
|
||||
cli.py `bincio edit` CLI
|
||||
server.py FastAPI write API for the edit drawer
|
||||
schema/
|
||||
bas-v1.schema.json JSON Schema for BAS
|
||||
SCHEMA.md Human-readable BAS spec
|
||||
site/ Astro project
|
||||
src/
|
||||
layouts/Base.astro
|
||||
pages/
|
||||
index.astro Activity feed (loads index.json client-side)
|
||||
activity/[id].astro Single activity (SSG, loads detail JSON client-side)
|
||||
stats/index.astro Heatmap + year totals
|
||||
components/
|
||||
ActivityFeed.svelte Card grid, sport filter, pagination
|
||||
ActivityDetail.svelte Map + stats + charts + photo gallery
|
||||
ActivityMap.svelte MapLibre GL (gradient track, linked hover dot)
|
||||
ActivityCharts.svelte Observable Plot (elevation/speed/HR/cadence tabs)
|
||||
StatsView.svelte Yearly heatmap + totals
|
||||
EditDrawer.svelte Slide-in edit panel (visible when PUBLIC_EDIT_URL set)
|
||||
lib/
|
||||
types.ts BAS TypeScript types
|
||||
format.ts formatDistance, formatDuration, sportIcon, etc.
|
||||
```
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
# Extract
|
||||
cd ~/src/bincio_activity
|
||||
uv run bincio extract --input ~/your-activity-data/activities --output /tmp/bincio_test
|
||||
|
||||
# Site dev server
|
||||
cd site
|
||||
ln -sf /tmp/bincio_test/_merged public/data # point at merged output
|
||||
cp .env.example .env && $EDITOR .env # set BINCIO_DATA_DIR
|
||||
npm run dev
|
||||
|
||||
# Edit server (optional — enables edit drawer in the site)
|
||||
uv run bincio edit --data-dir /tmp/bincio_test
|
||||
# set PUBLIC_EDIT_URL=http://localhost:4041 in site/.env
|
||||
|
||||
# Tests
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## MapLibre GL + Vite/Astro — known gotchas
|
||||
|
||||
Learnt the hard way during debugging (March 2026):
|
||||
|
||||
- **`maplibregl.workerUrl = ...` is the v3 API and silently no-ops in v4+.**
|
||||
The v5 API is `maplibregl.setWorkerUrl(url)`, but you don't need it at all in a
|
||||
normal Vite environment — MapLibre handles the blob worker automatically.
|
||||
|
||||
- **`optimizeDeps: { exclude: ['maplibre-gl'] }` breaks tile loading.**
|
||||
It prevents Vite from converting MapLibre's UMD bundle to ESM. The UMD bundle
|
||||
uses AMD `define()` internally; served raw, the tile worker blob fails silently →
|
||||
black map, no tiles. The correct setting is `include: ['maplibre-gl']`.
|
||||
|
||||
- **`build.target: 'es2022'` (and `optimizeDeps.esbuildOptions.target`) is required.**
|
||||
MapLibre's dependencies use ES2022 class field syntax. If esbuild downgrades it,
|
||||
helpers like `__publicField` aren't available inside the serialised worker blob
|
||||
scope → tile loading fails. This is a known upstream issue (maplibre-gl-js #6680).
|
||||
|
||||
- **Use static imports, not dynamic `await import('maplibre-gl')`, when possible.**
|
||||
With `client:only="svelte"` in Astro, SSR never runs for the component so there is
|
||||
no `window is not defined` risk. Static import lets Vite pre-bundle correctly.
|
||||
|
||||
- **Use `client:only="svelte"` (not `client:load`) for the activity detail page.**
|
||||
`client:load` does SSR + hydration; complex interactive components with MapLibre
|
||||
can hit hydration mismatch issues. `client:only` mounts fresh on the client only.
|
||||
|
||||
- **MapLibre v5 requires explicit `center` and `zoom` in the Map constructor.**
|
||||
v4 silently defaulted to `center: [0,0], zoom: 0`. v5 leaves internal projection
|
||||
state undefined → `Cannot read properties of undefined (reading 'lng')` crashes
|
||||
on any operation that touches coordinates (markers, resize, render). Always pass
|
||||
`center` and `zoom` even if you plan to `fitBounds` later.
|
||||
|
||||
- **MapLibre v5 requires `setLngLat()` on markers before `.addTo(map)`.**
|
||||
v4 tolerated markers without coordinates. v5 calls `Marker._update()` inside
|
||||
`addTo()`, which needs valid lngLat → same `'lng'` crash. Set a dummy `[0, 0]`
|
||||
if the real position arrives later (e.g. hover markers).
|
||||
|
||||
## Observable Plot — known gotchas
|
||||
|
||||
- **Curve names are hyphenated, not camelCase.**
|
||||
Use `"monotone-x"`, not `"monotoneX"`. Plot uses its own curve name registry
|
||||
(not raw d3 identifiers). Wrong names throw `unknown curve` at runtime.
|
||||
|
||||
The working `astro.config.mjs` Vite section:
|
||||
```js
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
include: ['maplibre-gl'],
|
||||
esbuildOptions: { target: 'es2022' },
|
||||
},
|
||||
build: { target: 'es2022' },
|
||||
},
|
||||
```
|
||||
|
||||
## Activity sidecar edits — design spec
|
||||
|
||||
Users edit activities via **sidecar markdown files** in the data dir.
|
||||
No database, no server — consistent with the project's static-files-only philosophy.
|
||||
|
||||
### File naming
|
||||
|
||||
```
|
||||
~/bincio_data/
|
||||
activities/{id}.json ← immutable extract output
|
||||
edits/{id}.md ← user edits (sidecar)
|
||||
edits/images/{id}/ ← uploaded photos
|
||||
_merged/ ← render-time merge output (gitignored-style)
|
||||
```
|
||||
|
||||
### Sidecar format
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Epic climb up Monte Grappa"
|
||||
sport: cycling
|
||||
hide_stats: [cadence]
|
||||
highlight: true
|
||||
private: false
|
||||
gear: "Trek Domane"
|
||||
---
|
||||
|
||||
Rode with friends. Legs felt great after the rest week...
|
||||
```
|
||||
|
||||
### Editing UX: drawer in Astro + `bincio edit` write API
|
||||
|
||||
- `bincio edit --data-dir ~/bincio_data` starts a FastAPI server on port 4041
|
||||
- Set `PUBLIC_EDIT_URL=http://localhost:4041` in `site/.env` to enable the edit button
|
||||
- Clicking Edit on any activity detail page opens a slide-in drawer
|
||||
- Saving writes the sidecar and triggers `merge_all()` automatically
|
||||
- `bincio render` always runs `merge_all()` before build/serve and symlinks `public/data` → `_merged/`
|
||||
|
||||
### `PUBLIC_EDIT_URL` as feature flag
|
||||
|
||||
- **Unset** → no Edit button, normal static site
|
||||
- **Set** → edit drawer enabled; lives in `site/.env` (gitignored)
|
||||
|
||||
## Known issues / next steps
|
||||
|
||||
- `bincio render` Python CLI is functional but `--watch` mode not yet implemented
|
||||
- Activity IDs in older test data may use `+0000` format (pre-fix); re-run extract to get `Z` format
|
||||
- Some activities appear with both untitled and titled IDs (near-dedup timing race)
|
||||
- Federation (remote data sources) not yet implemented in site
|
||||
- Friends pages (`/friends/{handle}/`) not yet implemented
|
||||
- The `site/.env` file is gitignored — copy from `site/.env.example`
|
||||
|
||||
## What "good" looks like (not yet done)
|
||||
|
||||
- [ ] `bincio render` Python CLI wraps `astro build` properly
|
||||
- [ ] Friends/federation pages in site
|
||||
- [ ] Personal records page
|
||||
- [ ] Activity search / full-text filter in feed
|
||||
- [ ] GitHub Actions template for auto-publish
|
||||
- [ ] Karoo/Garmin Connect importers beyond Strava
|
||||
- [x] `bincio.render.merge` — sidecar parser, `_merged/` output, private filter, highlight sort
|
||||
- [x] `bincio edit` FastAPI write API (GET/POST activity, image upload/delete, triggers merge)
|
||||
- [x] `EditDrawer.svelte` — slide-in edit UI in the Astro site
|
||||
- [x] `PUBLIC_EDIT_URL` feature flag
|
||||
- [x] Markdown rendering in activity description with image path rewriting
|
||||
- [x] Photo gallery with lightbox on activity detail page
|
||||
- [ ] `bincio render --watch` incremental rebuild on sidecar/data changes
|
||||
- [ ] Highlight badge in activity feed cards
|
||||
Reference in New Issue
Block a user