# BincioActivity — Architecture ## Overview BincioActivity is a two-stage pipeline that turns raw activity files (GPX, FIT, TCX) into a self-hosted static website. There is no database and no application server — everything is files. ``` Raw files ──► BAS data store ──► Static site extract render (Python) (Astro/Node) ``` --- ## Stage 1 — Extract `bincio extract` reads your activity files and writes a **BAS (BincioActivity Schema)** data store: a directory of JSON files. ```mermaid flowchart LR subgraph Input A[GPX files] B[FIT files] C[TCX files] D[activities.csv\nStrava metadata] end subgraph Extract ["bincio extract (Python)"] E[Parse] F[Compute metrics] G[Deduplicate] H[Write BAS JSON] end subgraph Output ["BAS data store ~/bincio_data/"] I[activities/\n*.json *.geojson] J[index.json\nall summaries] K[athlete.json\nzones + records] end A & B & C --> E D --> E E --> F --> G --> H H --> I & J & K ``` The data store is **immutable extract output** — never edited directly. --- ## Stage 2 — Render `bincio render` merges any user edits, then runs an Astro build to produce a static site. ```mermaid flowchart LR subgraph DataStore ["BAS data store"] A[activities/*.json] B[edits/*.md\nsidecar files] C[edits/images/] end subgraph Render ["bincio render (Astro/Node)"] D[merge_all\napply sidecars] E[Astro build\nSSG] end subgraph Site ["Static site dist/"] F[HTML pages] G[JS bundles] H[data/\nBAS JSON] end A & B & C --> D --> E --> F & G & H ``` The rendered site is **fully static**: no server needed to serve it. GitHub Pages, Netlify, nginx — all work. --- ## The edit flow When `bincio edit` is running locally, an **Edit** button appears in the site. It opens a drawer that writes sidecar files without touching the immutable extract output. ```mermaid sequenceDiagram actor User participant Site as Static site\n(Astro dev server) participant EditServer as bincio edit server\n(FastAPI, port 4041) participant DataStore as BAS data store User->>Site: clicks Edit on activity Site->>EditServer: GET /api/activity/{id} EditServer->>DataStore: reads .json + sidecar .md EditServer-->>Site: current values User->>Site: edits title / description / sport Site->>EditServer: POST /api/activity/{id} EditServer->>DataStore: writes edits/{id}.md EditServer->>DataStore: runs merge_all() EditServer-->>Site: ok Note over Site: feed reloads with merged data ``` The edit server is **never public-facing** — it only binds to `127.0.0.1` and is only enabled when `PUBLIC_EDIT_URL` is set in `site/.env`. --- ## Data sources There are three ways activities enter the data store: ```mermaid flowchart TD subgraph Sources A[📁 Local files\nGPX / FIT / TCX] B[🟠 Strava API\nOAuth sync] C[📱 Convert page\nPyodide in-browser] end subgraph EditServer ["bincio edit server\n(when running)"] D[POST /api/upload] E[POST /api/strava/sync] end subgraph CLI ["bincio extract CLI"] F[batch extract] end subgraph DataStore ["BAS data store"] G[activities/*.json] H[index.json] end A -->|bulk| F --> G & H A -->|single file via UI| D --> G & H B --> E --> G & H C -->|download JSON| A C -->|POST to edit server\nif configured| D ``` **Local files → CLI** is the primary path for bulk imports. **Strava sync** and **file upload** go through the edit server for single activities. **Convert page** runs the extract pipeline in-browser — output is either downloaded or sent to the edit server. --- ## The convert page `/convert/` is a page in the static site that runs the full extract pipeline **inside the browser** using Pyodide (Python compiled to WebAssembly). ```mermaid sequenceDiagram actor User as User\n(on phone or desktop) participant Page as /convert/ page\n(browser) participant Pyodide as Pyodide runtime\n(Python in WASM) participant EditServer as bincio edit server\n(optional) User->>Page: opens /convert/ Page->>Pyodide: load Pyodide + packages\n(lxml, fitdecode, bincio wheel) Note over Page,Pyodide: ~8MB, cached after first visit User->>Page: selects GPX / FIT / TCX file Page->>Pyodide: write file to virtual FS\nrun parse → metrics → write Pyodide-->>Page: BAS JSON + GeoJSON strings alt Download Page->>User: download activity.json\n(+ activity.geojson if GPS) else Save to bincio Page->>EditServer: POST /api/upload EditServer-->>Page: {id} Page->>User: redirect to activity page end ``` **"Save to bincio"** only appears when the page is served from an instance with `PUBLIC_EDIT_URL` set — the same flag that enables the edit drawer. For anyone else, downloading the JSON is the output. ### Why the convert page belongs to the instance The convert page is part of the static site build — it uses the same styling, the same Pyodide wheel, and (if enabled) the same edit server. It is a tool **for the people who use this instance**: - The instance owner uses it to convert files on mobile without needing a computer - (Future) other users of a multi-user instance use it to upload their own activities It is not a standalone public tool (though technically anyone with the URL could use the download path, since Pyodide runs locally in their browser). --- ## Federation Federation is how users follow each other across different bincio instances without any central server. ```mermaid flowchart TB subgraph InstanceA ["Instance A — bincio.alice.com"] A1[BAS data store] A2[Static site] A1 --> A2 end subgraph InstanceB ["Instance B — bincio.bob.com"] B1[BAS data store] B2[Static site] B1 --> B2 end subgraph Browser ["Bob's browser"] C[Feed] end A2 -->|BAS JSON URL| C B2 -->|BAS JSON URL| C Note1["Bob adds Alice's index.json URL\nto his followed feeds.\nNo accounts. No central server.\nJust a URL."] ``` Federation is a planned feature — the data format (BAS JSON) is designed for it, but the site UI doesn't yet support adding followed feeds. --- ## Mobile app (Capacitor) The existing Astro/Svelte site is wrapped in a **Capacitor** native shell to produce iOS and Android apps. No code is rewritten — Capacitor provides the native APIs that a plain browser PWA cannot. ### What Capacitor adds over a browser PWA | Capability | Browser PWA | Capacitor app | |---|---|---| | Background GPS (iOS) | ✗ killed by OS | ✅ native entitlement | | Background GPS (Android) | ⚠️ limited | ✅ foreground service | | Filesystem access | ✗ sandboxed | ✅ full device storage | | Install without App Store | ✅ | iOS: App Store / TestFlight | | Local HTTP server | ✗ | ✅ (future, via native plugin) | ### Architecture ```mermaid flowchart TD subgraph App ["Capacitor app (iOS / Android)"] subgraph WebView ["WebView — existing Astro/Svelte site"] A[/record/ — GPS recorder] B[/convert/ — Pyodide converter] C[/feed/, /activity/, etc.] end subgraph Native ["Native layer"] D[Geolocation plugin\nbackground GPS] E[Filesystem plugin\nread/write device storage] end end subgraph Cloud ["Cloud instance (optional)"] F[Static site] G[bincio edit server] end A -->|JS bridge| D B -->|JS bridge| E B -->|POST /api/import-bas| G C --> F ``` ### Mobile workflows **Workflow 1 — Record on phone, save to cloud** ``` Phone app: /record/ → GPS recording (Capacitor Geolocation) → export GPX → /convert/ /convert/ → Pyodide runs extract pipeline in WebView → POST /api/import-bas to cloud edit server Cloud instance → saves activity, site updates ``` **Workflow 2 — Record on phone, save locally (offline)** ``` Phone app: /record/ → GPS recording → export GPX → /convert/ /convert/ → Pyodide converts in WebView → download BAS JSON to device storage (upload to cloud later when online) ``` **Workflow 3 — Import from OsmAnd / Organic Maps** ``` OsmAnd / Organic Maps → exports GPX to device storage Phone app: /convert/ → user picks GPX file → Pyodide converts in WebView → save to cloud or download ``` **Workflow 4 — Fully offline on phone (future)** ``` Phone app: /record/ → GPS recording (native Geolocation) → export GPX → /convert/ /convert/ → Pyodide converts (loaded from local cache) → writes BAS JSON to device storage (Filesystem plugin) Local Node server → serves BAS JSON from device storage to WebView WebView → feed + activity pages read from local server → edit drawer saves sidecars to device storage ``` See "Fully offline — missing pieces" below for implementation status. ### Setup ```bash cd site # First time: initialise native projects npx cap add android # creates site/android/ npx cap add ios # creates site/ios/ — requires macOS + Xcode # Daily workflow npm run cap:sync # build Astro + sync to native projects npm run cap:android # build + open Android Studio npm run cap:ios # build + open Xcode ``` ### The bincio Python wheel The `/convert/` page loads the extract pipeline via **Pyodide** (Python compiled to WebAssembly). The pipeline is bundled as `site/public/bincio.whl` — a standard Python wheel that Pyodide loads via `micropip`. To rebuild the wheel after changing the extract code: ```bash # from the repo root uv build --wheel cp dist/bincio-*.whl site/public/bincio.whl ``` Pyodide loads these packages on first visit (cached by the browser after that): - `lxml`, `pyyaml` — prebuilt Pyodide packages (~2 MB) - `fitdecode`, `gpxpy`, `rdp` — pure Python, installed via micropip - `bincio.whl` — our wheel, ~150 KB Total cold-start download: ~10 MB. Subsequent visits: instant (all cached). ### Fully offline — missing pieces Five things need to be in place before Workflow 4 works end-to-end. #### 1. Pyodide available offline Currently loads from CDN on first use. Two options: - **Service worker cache** *(recommended)* — on first online visit the service worker caches all Pyodide assets; subsequent visits work offline. Standard PWA pattern, no app size increase. - **Bundle in app assets** — copy `pyodide/` into `site/public/` at build time. Adds ~25 MB to the app but works with zero network dependency. Use if targeting environments with no initial connectivity. Status: not yet implemented. #### 2. Write converted activities to device storage `@capacitor/filesystem` is already installed. After Pyodide converts a file, instead of (or in addition to) POSTing to the edit server, the convert page should: 1. Write `{id}.json` and `{id}.geojson` to a local `activities/` directory via the Filesystem plugin 2. Append the summary to a local `index.json` This is self-contained JS work — no native changes needed. Status: not yet implemented. #### 3. Serve local activity data to the WebView *(the core problem)* The Astro site reads `/data/index.json` and `/data/activities/*.json` via `fetch()`. In the current setup these come from either bundled static assets (baked in at build time) or a remote server. There is no built-in mechanism for a WebView to dynamically serve locally-stored files at those URLs. Two approaches: **Option A — Service worker interception** *(web-only, no native code)* A service worker intercepts `fetch('/data/*')` and responds with data from IndexedDB. No native plugin required. Limitation: iOS WKWebView has historically restricted service worker scope in Capacitor apps, though this has improved in recent iOS versions. Best option if cross-platform parity is not critical. **Option B — Local HTTP server via `capacitor-nodejs`** *(recommended for iOS reliability)* [`capacitor-nodejs`](https://github.com/hampoelz/Capacitor-NodeJS) embeds a Node.js runtime inside the Capacitor app. A small Express/Fastify server: - Serves the static site assets (or proxies to the WebView asset loader) - Serves BAS JSON from device storage at `/data/*` - Handles the edit/save API (`/api/import-bas`, `/api/activity/{id}`, etc.) The WebView points to `http://localhost:PORT/`. Reliable on both iOS and Android. Status: not yet implemented. Option B is the recommended path. #### 4. Port edit server API to local JS Once Option B above is in place, the Node.js server needs to implement the same API surface as the current FastAPI edit server — at minimum: | Endpoint | Purpose | |---|---| | `POST /api/import-bas` | Save converted activity to device storage | | `GET /api/activity/{id}` | Read activity + sidecar | | `POST /api/activity/{id}` | Write sidecar to device storage | | `GET /data/index.json` | Serve merged index from device storage | | `GET /data/activities/*` | Serve individual activity files | The sidecar merge logic (`merge_all`) can either be reimplemented in JS (~150 lines) or invoked via Pyodide (already present in the app). Strava sync and image upload require network access by definition — they are out of scope for offline mode. Status: not yet implemented. #### 5. Summary | Step | Effort | Status | |---|---|---| | Pyodide service worker cache | Medium | Not started | | Write to Filesystem after convert | Easy | Not started | | Local Node.js server (`capacitor-nodejs`) | Hard | Not started | | Port edit API to local JS | Medium | Not started | | Sidecar merge in JS (or via Pyodide) | Medium | Not started | The local Node.js server (step 3B) is the critical path item — it unblocks steps 4 and makes the feed dynamic. Steps 1 and 2 are independent and can be done in any order. --- ### The `/api/import-bas` endpoint The `/convert/` page sends pre-converted BAS JSON directly to the edit server, avoiding the need to re-parse a file on the server side. The endpoint: 1. Validates the `id` field 2. Writes `activities/{id}.json` (and `.geojson` if provided) 3. Rebuilds `index.json` 4. Runs `merge_all()` --- ## Deployment ```mermaid flowchart LR subgraph Local ["Local machine"] A[bincio extract] B[bincio edit\noptional] end subgraph VPS ["VPS or static host"] C[Static site\ndist/] D[nginx] end subgraph Phone ["Phone"] E[OsmAnd /\nOrganic Maps] F[Browser:\n/convert/] end A -->|rsync / CI| C C --> D E -->|GPX export| F F -->|upload| B B --> A ``` The edit server is **always local** in the single-user setup. In a future multi-user deployment on a VPS, it would run as a service alongside nginx.