97c7fae9be
- Add /api/activity/{id}/geojson and /api/activity/{id}/timeseries endpoints
(bearer-token-gated, falls back from _merged to raw activities dir)
- Rewrite activity detail screen with MapLibreGL v11 API (Map, Camera,
GeoJSONSource, Layer) and react-native-svg area chart with gradient fill
- On-demand fetch for remote activities that have no local geojson/timeseries
- Add react-native-svg dependency; requires dev build (npx expo run:android)
676 lines
26 KiB
Markdown
676 lines
26 KiB
Markdown
# Bincio Mobile App — Design Document
|
||
|
||
## Vision
|
||
|
||
The long-term goal is full independence from Garmin Connect, Strava, Hammerhead,
|
||
and similar platforms. Today those platforms act as mandatory intermediaries: your
|
||
device syncs to their cloud, you authorise third parties to pull from their API,
|
||
and your data effectively lives on their servers.
|
||
|
||
The Bincio mobile app removes that dependency:
|
||
|
||
- Your FIT/GPX/TCX files live on your device.
|
||
- The app reads them directly — no platform sync required.
|
||
- A Bincio instance (bincio.org or self-hosted) is an optional upgrade for backup,
|
||
sharing, and web access — not a prerequisite.
|
||
- Devices like the **Karoo 2** (Android-based) are a first-class target: activities
|
||
are already saved locally as FIT files, so the app can pick them up directly from
|
||
the filesystem without any export step.
|
||
|
||
This initial version focuses on **post-ride import and local storage**. Live
|
||
recording (GPS + sensors during a ride) is the long-term goal that would complete
|
||
full platform independence, but it is out of scope until the foundation is solid.
|
||
|
||
---
|
||
|
||
## Philosophy
|
||
|
||
**Local-first.** All activity data lives on the device. The app works fully offline
|
||
— no account, no internet connection, no platform authorisation required.
|
||
|
||
**Original files as source of truth.** The raw FIT/GPX/TCX file is always stored on
|
||
device alongside the extracted BAS JSON. This means:
|
||
|
||
- You can re-extract at any time (e.g. when the algorithm improves, or to apply DEM
|
||
correction after connecting to an instance).
|
||
- Sync to a remote instance is just pushing the original file — the server
|
||
re-extracts with the full Python pipeline.
|
||
- No data is ever locked into a proprietary representation.
|
||
|
||
**The algorithm travels to the data — not the other way around.** When internet is
|
||
available, the app downloads a fresh copy of the extraction algorithm from bincio.org
|
||
and runs it locally. Your activity files never touch the server. Only the Python
|
||
wheel (the code) is downloaded; the data stays on device.
|
||
|
||
**Sync is optional and explicit.** Connecting to a Bincio instance adds cloud
|
||
backup, the web feed, and sharing. The app never silently overwrites local data.
|
||
Sync is user-initiated.
|
||
|
||
**Open format.** Activities are stored in the BAS schema — the same JSON format the
|
||
server uses. Any tool in any language can read them.
|
||
|
||
---
|
||
|
||
## Development setup
|
||
|
||
### Two build modes: Expo Go vs Development Build
|
||
|
||
This is the most important thing to understand before starting.
|
||
|
||
**Expo Go** is the Expo app available on the Play Store / App Store. It runs any
|
||
Expo project by scanning a QR code — no compilation step. However, it only supports
|
||
Expo's own built-in modules. It does **not** support third-party native modules.
|
||
|
||
**Development Build** is a custom version of the Expo Go app compiled specifically
|
||
for this project. It includes all third-party native modules (react-native-webview,
|
||
maplibre). It is installed once on the device; after that, code changes still update
|
||
instantly via Metro (the JS bundler) — no rebuild needed.
|
||
|
||
| | Expo Go | Development Build |
|
||
|---|---|---|
|
||
| Setup | Scan QR, instant | Build APK once (local or EAS cloud) |
|
||
| expo-sqlite | ✅ | ✅ |
|
||
| expo-document-picker | ✅ | ✅ |
|
||
| react-native-webview (Pyodide) | ✗ | ✅ |
|
||
| @maplibre/maplibre-react-native | ✗ | ✅ |
|
||
| Code changes | instant (Metro) | instant (Metro) |
|
||
| Native changes | need new Expo Go release | rebuild APK |
|
||
|
||
**Phase 0 and 0.5** only use built-in Expo modules — Expo Go works. **Phase 1**
|
||
(Pyodide) and **Phase 4** (MapLibre maps) require a Development Build because
|
||
`react-native-webview` and `@maplibre/maplibre-react-native` are native modules.
|
||
|
||
The preferred path for Phase 1+: connect the phone via USB and run
|
||
`npx expo run:android` once. After that, JS changes still update instantly via Metro
|
||
— no rebuild needed unless you change native code.
|
||
|
||
---
|
||
|
||
### Prerequisites
|
||
|
||
| Tool | Required for | Install |
|
||
|---|---|---|
|
||
| Node.js 20 LTS | everything | [nodejs.org](https://nodejs.org) or `nvm install 20` |
|
||
| npm | everything | ships with Node |
|
||
| Android Studio | Android dev build / emulator | [developer.android.com/studio](https://developer.android.com/studio) |
|
||
| Xcode 15+ | iOS only, macOS only | App Store → `xcode-select --install` |
|
||
| EAS CLI | cloud builds (optional) | `npm install -g eas-cli` |
|
||
|
||
You do **not** need a physical Android device to start. The Android emulator
|
||
(AVD Manager inside Android Studio) works fine for development.
|
||
|
||
---
|
||
|
||
### First-time setup
|
||
|
||
```bash
|
||
# From the repo root:
|
||
bash mobile/setup.sh
|
||
```
|
||
|
||
The script checks Node, Android SDK, and Xcode availability; installs npm
|
||
dependencies; and generates the required Expo type declarations.
|
||
|
||
---
|
||
|
||
### Phase 0 — Expo Go (quickest start)
|
||
|
||
Since Phase 0 uses only built-in Expo modules, you can start with Expo Go:
|
||
|
||
```bash
|
||
cd mobile
|
||
npx expo start
|
||
```
|
||
|
||
1. Install **Expo Go** on your Android phone from the Play Store.
|
||
2. Scan the QR code printed in the terminal.
|
||
3. The app loads instantly. Code changes in your editor appear on the phone
|
||
within a second or two.
|
||
|
||
> **Limitation**: once you add the Pyodide WebView in Phase 1, you must switch to
|
||
> a Development Build. Expo Go will show an error for `react-native-webview`.
|
||
|
||
---
|
||
|
||
### Phase 1+ — Development Build
|
||
|
||
#### Option A: local build (Android Studio required)
|
||
|
||
Plug in an Android device via USB (or start an emulator in Android Studio), then:
|
||
|
||
```bash
|
||
cd mobile
|
||
npx expo run:android # builds APK, installs it, starts Metro
|
||
```
|
||
|
||
This compiles the full native project once (~3–5 min). After that, JS changes
|
||
reflect instantly without rebuilding.
|
||
|
||
For the emulator, create an AVD in Android Studio with API 33+ and start it before
|
||
running the command.
|
||
|
||
#### Option B: local EAS build (no Android Studio, no external cloud)
|
||
|
||
`eas build --local` runs the entire build pipeline on your own machine (or VPS):
|
||
|
||
```bash
|
||
npm install -g eas-cli
|
||
eas build -p android --profile development --local
|
||
```
|
||
|
||
This produces an `.apk` you can transfer to the device via any means (USB, VPS
|
||
download link, AirDrop). No Expo account or cloud service required.
|
||
|
||
---
|
||
|
||
### iOS development (macOS only)
|
||
|
||
```bash
|
||
cd mobile
|
||
npx expo run:ios # opens iOS simulator, builds, and runs
|
||
```
|
||
|
||
Requires Xcode 15+ and an active iOS simulator. Cloud builds via EAS:
|
||
|
||
```bash
|
||
eas build -p ios --profile development # requires Apple Developer account ($99/yr)
|
||
```
|
||
|
||
---
|
||
|
||
### Where Pyodide comes from
|
||
|
||
The hidden WebView loads Pyodide from the **jsDelivr CDN** — the same source
|
||
as the `/convert/` page on the web:
|
||
|
||
```
|
||
https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js (~30 MB)
|
||
```
|
||
|
||
On first extraction after install, the WebView downloads and caches this
|
||
runtime in `expo-file-system`'s document directory. Subsequent extractions use
|
||
the cached copy — no internet required.
|
||
|
||
The **bincio wheel** (~50 KB) is fetched from:
|
||
|
||
```
|
||
GET {instance_url}/api/wheel/version → { version, url, api_url }
|
||
GET {instance_url}/bincio-{version}-py3-none-any.whl (nginx, prod)
|
||
GET {instance_url}/api/wheel/download (FastAPI, local dev)
|
||
```
|
||
|
||
If no instance is configured, it falls back to `https://bincio.org`. The wheel
|
||
is also cached locally and re-downloaded only when the version changes.
|
||
|
||
**Local development** (before bincio is published to PyPI): the wheel is not on
|
||
PyPI, so there is a bundled fallback at `mobile/assets/bincio.whl`. The
|
||
extraction code loads it from the app bundle when no cached wheel exists yet.
|
||
This bundled copy is updated manually by running:
|
||
|
||
```bash
|
||
uv build --wheel # builds dist/bincio-*.whl
|
||
cp dist/bincio-*.whl mobile/assets/bincio.whl
|
||
```
|
||
|
||
The server-side `GET /api/wheel/download` endpoint also serves the wheel
|
||
directly from `dist/` — useful when running a local `bincio serve` instance on
|
||
the same WiFi network as the test device and wanting to exercise the update flow.
|
||
|
||
The **common packages** (`fitdecode`, `gpxpy`, `lxml`, `pyyaml`) are fetched from
|
||
the Pyodide CDN via micropip on first use and cached by the WebView's internal
|
||
storage.
|
||
|
||
**Summary of what touches the network:**
|
||
|
||
| Asset | Size | When | Cached |
|
||
|---|---|---|---|
|
||
| Pyodide runtime | ~30 MB | once (first extraction ever) | ✅ permanently |
|
||
| Common packages | ~5 MB | once | ✅ permanently |
|
||
| bincio wheel | ~50 KB | on version bump (bundled fallback in assets/) | ✅ until next update |
|
||
| Map tiles | per-tile | on pan/zoom | ✅ by MapLibre |
|
||
|
||
Everything else — the activity files, the extracted BAS JSON — stays on device.
|
||
|
||
---
|
||
|
||
### Distributing the app
|
||
|
||
| Target | Method |
|
||
|---|---|
|
||
| Your own Android phone | `npx expo run:android` via USB, or EAS development build |
|
||
| Karoo 2 | EAS production build → download APK → sideload via `adb install bincio.apk` or Karoo's app sideloader |
|
||
| Other Android users | EAS build → share APK download link (no Play Store needed) |
|
||
| Play Store | EAS production build → upload `.aab` to Play Console |
|
||
| iOS users | EAS build → TestFlight (beta) or App Store |
|
||
|
||
For Karoo sideloading:
|
||
```bash
|
||
eas build -p android --profile preview # produces a standalone APK
|
||
adb install /path/to/bincio.apk # with Karoo connected via USB
|
||
```
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
The mobile app lives in `mobile/` inside the main bincio repository (Option A).
|
||
This keeps it close to the bincio wheel it depends on and makes it easy to test
|
||
algorithm changes end-to-end. It can be extracted to its own repository later.
|
||
|
||
```
|
||
bincio_activity/
|
||
├── bincio/ — Python server + extractor
|
||
├── site/ — Astro web frontend
|
||
├── mobile/ — Expo React Native app ← this document
|
||
│ ├── app/ — Expo Router screens
|
||
│ ├── components/ — shared React Native components
|
||
│ ├── db/ — SQLite schema and queries
|
||
│ ├── extraction/ — WebView host + Pyodide bridge
|
||
│ └── sync/ — push/pull logic
|
||
└── docs/
|
||
```
|
||
|
||
---
|
||
|
||
## What already exists
|
||
|
||
| Piece | Where | Notes |
|
||
|---|---|---|
|
||
| BAS schema | `docs/schema.md` | The on-device data format — identical to the server format |
|
||
| Pyodide-based extraction | `site/src/pages/convert/` | FIT/GPX/TCX parsing via CPython→WASM in the browser — **the proof of concept for mobile extraction**. A hidden WebView uses the same mechanism. |
|
||
| Bincio wheel | `dist/bincio-0.1.0-py3-none-any.whl`, served at `/bincio-0.1.0-py3-none-any.whl` | Pure-Python wheel already downloaded and run by the `/convert/` page |
|
||
| Local storage concept | `site/src/pages/convert/` | IndexedDB + service worker in the web app. Mobile uses SQLite instead. |
|
||
| Content-addressed dedup | `bincio/extract/dedup.py` | `source_hash` (SHA-256 of raw file) prevents duplicates |
|
||
| REST API | `bincio/serve/server.py` | Login, upload, activity detail, index.json — sync primitives already there |
|
||
| Settings table | `bincio/serve/db.py` | Key/value settings in the server DB; same pattern used on device |
|
||
|
||
---
|
||
|
||
## Technology
|
||
|
||
### Framework: Expo (React Native)
|
||
|
||
- TypeScript throughout
|
||
- `expo-sqlite` v2 — on-device SQLite with WAL mode
|
||
- `expo-document-picker` — file picking from device storage
|
||
- `expo-file-system` — filesystem access (critical for Karoo directory watching on Android)
|
||
- `react-native-webview` — hidden WebView for Pyodide
|
||
- `@maplibre/maplibre-react-native` — maps, same tile standard as the web app
|
||
- `expo-background-fetch` + `expo-task-manager` — background directory polling (Android)
|
||
- `expo-notifications` — import notifications
|
||
- EAS Build — iOS and Android binaries; APK sideloading for Karoo
|
||
|
||
---
|
||
|
||
## Extraction: Pyodide in a hidden WebView
|
||
|
||
The `/convert/` page already demonstrates that the full Python extraction pipeline
|
||
runs in a browser via **Pyodide** (CPython compiled to WebAssembly). A React Native
|
||
app can host a hidden `WebView` running the exact same environment. No rewrite of
|
||
the extraction logic is required.
|
||
|
||
### Package stack (proven in /convert/ today)
|
||
|
||
```
|
||
Pyodide v0.26 (CPython → WASM, ~30 MB)
|
||
├── lxml — pre-compiled WASM in Pyodide (XML / GPX parsing)
|
||
├── fitdecode — pure Python, installed via micropip (FIT parsing)
|
||
├── gpxpy — pure Python, installed via micropip (GPX parsing)
|
||
├── pyyaml — pure Python, installed via micropip
|
||
└── bincio wheel — pure Python, fetched from bincio.org
|
||
```
|
||
|
||
Every dependency is either pre-compiled in Pyodide or **pure Python with no C
|
||
extensions**. Nothing needs recompilation for mobile.
|
||
|
||
### Data flow
|
||
|
||
```
|
||
React Native
|
||
1. Read file bytes from device filesystem (expo-file-system)
|
||
2. postMessage({ type: 'extract', filename, bytes }) → hidden WebView
|
||
|
||
Hidden WebView (Pyodide)
|
||
3. Write bytes to Pyodide virtual FS (/tmp/activity.fit)
|
||
4. Run Python extraction → BAS dict (detail + timeseries + geojson)
|
||
5. postMessage({ type: 'result', detail, timeseries, geojson }) → RN
|
||
|
||
React Native
|
||
6. Store detail_json, timeseries_json, geojson in SQLite
|
||
7. Copy original file to app storage → record path in DB
|
||
```
|
||
|
||
**Data never leaves the device.** Network traffic: only the Pyodide runtime
|
||
(~30 MB, CDN, cached once) and the bincio wheel (~50 KB, from bincio.org, updated
|
||
on version bump).
|
||
|
||
### Algorithm updates without App Store releases
|
||
|
||
The bincio wheel is versioned. On app startup the app calls:
|
||
|
||
```
|
||
GET /api/wheel/version → { "version": "0.2.1", "url": "/bincio-0.2.1-py3-none-any.whl" }
|
||
```
|
||
|
||
If the cached wheel is outdated, the new one is downloaded and the next extraction
|
||
uses the updated algorithm. Improvements to hysteresis, DEM correction, or lap
|
||
detection reach all devices within hours of a server deployment.
|
||
|
||
### Performance
|
||
|
||
| Scenario | Time |
|
||
|---|---|
|
||
| First extraction (cold Pyodide + packages) | ~5–8 s |
|
||
| First extraction in session (warm WebView) | ~1–3 s |
|
||
| Subsequent extractions (warm WebView) | ~0.5–1 s |
|
||
| Pyodide RAM while active | ~100–150 MB |
|
||
|
||
For batch import the WebView is kept alive across files; per-file cost drops to
|
||
the Python execution time only.
|
||
|
||
---
|
||
|
||
## Android vs iOS: platform divergences
|
||
|
||
These two platforms share almost all code. The differences are confined to
|
||
filesystem access and background behaviour.
|
||
|
||
### Filesystem access
|
||
|
||
| | Android | iOS |
|
||
|---|---|---|
|
||
| App sandbox | App has its own private directory | App has its own private directory |
|
||
| External paths | Can read arbitrary paths on the filesystem with `READ_EXTERNAL_STORAGE` (≤ Android 12) or `READ_MEDIA_*` scoped permissions (Android 13+) | **Fully sandboxed.** No access to paths outside the app container or Files app |
|
||
| Karoo rides dir | `expo-file-system` can read `/sdcard/Karoo/Rides/` directly once permission is granted | Not possible |
|
||
| Manual import | Document picker or share sheet | Document picker or share sheet |
|
||
|
||
### Auto-import (Phase 2)
|
||
|
||
| | Android | iOS |
|
||
|---|---|---|
|
||
| Mechanism | Poll a configured directory path every few minutes via a background task | Not possible — iOS apps cannot read external directories |
|
||
| Background execution | `expo-background-fetch` fires reliably; Android allows longer background windows | Background fetch is capped at ~30 s and is not guaranteed to fire; effectively unavailable |
|
||
| Import trigger | Automatic on new FIT file detected in watched directory | Manual: user shares file via Files app or "Open with Bincio" |
|
||
| Karoo auto-import | ✅ Full support — configure path once, rides appear automatically | ✗ Not applicable (Karoo is Android) |
|
||
|
||
### Receiving files from other apps (share sheet)
|
||
|
||
| | Android | iOS |
|
||
|---|---|---|
|
||
| Mechanism | Android Intent filter: `android.intent.action.SEND` for `.fit`, `.gpx`, `.tcx` | iOS Share Extension (Expo supports this via `expo-intent-launcher` / config plugin) |
|
||
| User experience | "Open with Bincio" in any file manager or app | "Share → Bincio" from Files, Komoot, etc. |
|
||
|
||
### App distribution
|
||
|
||
| | Android | iOS |
|
||
|---|---|---|
|
||
| APK sideloading | ✅ Supported — critical for Karoo (no Google Play) | ✗ Not allowed |
|
||
| Store | Google Play (optional) | App Store required (or TestFlight for beta) |
|
||
| Karoo installation | Sideload APK directly onto the device | N/A |
|
||
|
||
### WebView (Pyodide)
|
||
|
||
| | Android | iOS |
|
||
|---|---|---|
|
||
| WebView engine | Chrome WebView (system-provided, updateable) | WKWebView (WebKit, part of iOS) |
|
||
| WASM JIT | ✅ Full JIT via V8 | ✅ JIT allowed in WKWebView (Apple's exception for browser engine components) — works from iOS 14 |
|
||
| Memory limit | ~1 GB+ on modern Android | Varies by device; typically 300–600 MB. Pyodide (~150 MB) fits comfortably on iPhone XS and later |
|
||
| Performance | Slightly faster (V8 WASM JIT) | Adequate; extraction of a 1-hour FIT file well under 3 s on modern iPhone |
|
||
|
||
### Summary: what is Android-only
|
||
|
||
- Auto-import from a watched directory (Phase 2)
|
||
- `auto_import_path` setting (hidden in the UI on iOS)
|
||
- APK sideloading (for Karoo)
|
||
|
||
Everything else — extraction, local feed, activity detail, sync — is identical on
|
||
both platforms.
|
||
|
||
---
|
||
|
||
## Data model on device
|
||
|
||
```sql
|
||
CREATE TABLE activities (
|
||
id TEXT PRIMARY KEY, -- BAS ID: "2026-04-17T074238Z"
|
||
source_hash TEXT NOT NULL, -- SHA-256 of raw file (dedup key)
|
||
detail_json TEXT NOT NULL, -- full BAS detail JSON blob
|
||
timeseries_json TEXT, -- 1 Hz arrays, loaded lazily
|
||
geojson TEXT, -- simplified GPS track
|
||
original_path TEXT, -- path in app storage (NULL if pulled from server)
|
||
synced_at INTEGER, -- unix timestamp of last push (NULL = unsynced)
|
||
origin TEXT NOT NULL -- "local" | "remote"
|
||
CHECK(origin IN ('local', 'remote')),
|
||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||
);
|
||
|
||
CREATE TABLE settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
**Settings keys:**
|
||
|
||
| Key | Description | Platform |
|
||
|---|---|---|
|
||
| `instance_url` | e.g. `https://bincio.org` | Both |
|
||
| `handle` | User's handle on the remote instance | Both |
|
||
| `session_token` | Bearer token for API auth | Both |
|
||
| `last_sync_at` | ISO timestamp of last sync | Both |
|
||
| `wheel_version` | Cached bincio wheel version | Both |
|
||
| `auto_import_path` | Directory to watch for new FIT files | **Android only** |
|
||
|
||
---
|
||
|
||
## Sync protocol
|
||
|
||
Sync is a two-way hash-based diff. No custom protocol is needed beyond the
|
||
existing REST API.
|
||
|
||
### Push (local → server)
|
||
|
||
1. `GET {instance_url}/{handle}/index.json` — collect remote activity IDs.
|
||
2. Find local rows where `synced_at IS NULL` and `original_path IS NOT NULL`.
|
||
3. `POST /api/upload` with the original file for each.
|
||
4. On 200: set `synced_at = unixepoch()`.
|
||
|
||
### Pull (server → local)
|
||
|
||
1. `GET {instance_url}/{handle}/index.json` (+ yearly shards if present).
|
||
2. Find remote IDs absent from local DB.
|
||
3. For each missing activity:
|
||
- `GET …/activities/{id}.json` → `detail_json`
|
||
- `GET …/activities/{id}.timeseries.json` → `timeseries_json`
|
||
- `GET …/activities/{id}.geojson` → `geojson`
|
||
4. Insert with `origin = 'remote'`, `synced_at = unixepoch()`.
|
||
|
||
Activities pulled from the server have no local `original_path`. Re-extraction
|
||
requires the original file to be available (either already on device or fetched
|
||
from the instance if it stored it).
|
||
|
||
### Conflict handling
|
||
|
||
Activities are immutable. `source_hash` is the dedup key: if the same file arrives
|
||
at the server twice, the second upload is rejected with 409.
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
The server currently uses session cookies. For mobile, Bearer tokens are cleaner.
|
||
A new endpoint is needed (Phase 3 server work):
|
||
|
||
```
|
||
POST /api/auth/token
|
||
Body: { "handle": "…", "password": "…" }
|
||
→ { "token": "abc123…", "expires_at": "2027-04-24T00:00:00Z" }
|
||
```
|
||
|
||
The token is stored in the `settings` table and sent as
|
||
`Authorization: Bearer abc123…` on all API requests.
|
||
|
||
---
|
||
|
||
## Implementation plan
|
||
|
||
### Phase 0 — Foundation ✅
|
||
*Goal: app launches, settings can be configured, a BAS JSON file can be picked
|
||
and displayed as an activity card. No extraction yet.*
|
||
|
||
- Expo Router with three tabs: **Feed**, **Import**, **Settings**
|
||
- `expo-sqlite` initialised; `activities` and `settings` tables created on first launch
|
||
- Settings screen: instance URL and handle, saved to `settings` table
|
||
- Import screen: `expo-document-picker`; BAS `.json` files parsed and inserted into feed
|
||
- Feed screen: activity cards sorted by `started_at`, sport icon, distance, elevation
|
||
- `GET /api/wheel/version` server endpoint (public, no auth)
|
||
|
||
**Done when:** App launches, user picks a `.json` BAS file, sees it in the Feed. ✅
|
||
|
||
---
|
||
|
||
### Phase 0.5 — Remote feed sync ✅
|
||
*Goal: pull all activities from a remote bincio instance into the local feed.*
|
||
|
||
- `POST /api/auth/token` — password login returning a Bearer token (stored in
|
||
SQLite; password forgotten immediately after)
|
||
- `GET /api/feed` — auth-gated; reads `_merged/index.json` shards and returns
|
||
all activity summaries as JSON
|
||
- Settings screen: Connect section (password field + Connect button + status)
|
||
- Feed screen: **↓ Sync** button and pull-to-refresh; "cloud" badge on remote
|
||
activities; `syncFeed()` upserts remote summaries without overwriting local imports
|
||
|
||
**Done when:** Tap Connect, tap Sync, all instance activities appear in the Feed. ✅
|
||
|
||
---
|
||
|
||
### Phase 1 — Local FIT/GPX/TCX extraction via Pyodide
|
||
*Goal: pick a FIT/GPX/TCX file, extract it on-device in ~5 s.*
|
||
|
||
**Requires a Development Build** (`npx expo run:android` via USB, or
|
||
`eas build --local`). Expo Go does not support `react-native-webview`.
|
||
|
||
**Extraction engine (`mobile/extraction/`):**
|
||
- `PyodideWebView.tsx` — hidden `WebView` rendering an inline HTML page that
|
||
bootstraps Pyodide
|
||
- `wheelCache.ts` — on startup, `GET /api/wheel/version`; if version changed,
|
||
download and store wheel in `expo-file-system` app directory; falls back to
|
||
bundled `assets/bincio.whl` for offline / pre-deploy use
|
||
- `extractActivity.ts` — encodes file bytes as base64, sends via `postMessage`,
|
||
awaits `{ detail, timeseries, geojson }` response
|
||
- Loading state: "Warming up extractor…" shown only on very first use
|
||
|
||
**Import screen (full):**
|
||
- Picks FIT/GPX/TCX, passes to `extractActivity`, stores result in SQLite
|
||
(`detail_json`, `timeseries_json`, `geojson` columns)
|
||
- Copies original file to `{documentDirectory}/originals/{source_hash}.{ext}`
|
||
- Duplicate detection via `source_hash` before extraction
|
||
|
||
**Done when:** Pick a FIT file from the Karoo rides folder, see full stats in
|
||
the Feed within ~5 s, including map and elevation profile.
|
||
|
||
---
|
||
|
||
### Phase 2 — Karoo auto-import *(Android only)*
|
||
*Goal: finish a ride, connect to WiFi, the activity appears in Bincio automatically.*
|
||
|
||
**Android:**
|
||
- Settings screen gains `auto_import_path` field (Android only, hidden on iOS)
|
||
- `expo-task-manager` background task registered at app startup
|
||
- Task polls `auto_import_path` every 5 minutes; for each `.fit` file whose
|
||
`source_hash` is not in the DB, triggers extraction and import
|
||
- `expo-notifications` sends a local notification: "New ride: Morning Ride — 45 km"
|
||
|
||
**iOS (alternative flow for Phase 2):**
|
||
- Share Extension config so "Open with Bincio" appears in the iOS Files app
|
||
- Tapping it hands the file to the app, which runs extraction immediately
|
||
- No background polling; user-initiated but one-tap
|
||
|
||
**Done when (Android):** Finish a ride on the Karoo, the activity appears in
|
||
Bincio within 5 minutes of connecting to WiFi, with no manual action.
|
||
|
||
---
|
||
|
||
### Phase 3 — Push sync
|
||
*Goal: locally imported activities appear on the remote instance after one tap.*
|
||
|
||
Auth (Bearer token + Connect UI) is already done in Phase 0.5. Remaining work:
|
||
|
||
**Server:**
|
||
- `POST /api/upload` accepting a raw FIT/GPX/TCX file with Bearer token auth —
|
||
same as the existing web upload endpoint but token-gated
|
||
|
||
**App:**
|
||
- Push button (Settings or Feed header): iterates unsynced local activities
|
||
(`synced_at IS NULL AND origin = 'local'`), uploads original file, marks synced
|
||
- Progress indicator per activity; useful for first push with many files
|
||
|
||
**Done when:** Tap **Push**, locally imported activities appear on bincio.org.
|
||
|
||
---
|
||
|
||
### Phase 4 — Activity detail: map + elevation chart
|
||
*Goal: every activity shows a route map and elevation profile.*
|
||
|
||
**Requires a Development Build** — `@maplibre/maplibre-react-native` is a native
|
||
module. Same dev build used for Phase 1 covers Phase 4.
|
||
|
||
**Data strategy (on-demand fetch for remote activities):**
|
||
- Local activities (Phase 1 imports): `geojson` and `timeseries_json` stored in
|
||
SQLite — map and chart render immediately, no network needed
|
||
- Remote activities (Phase 0.5 synced): detail screen fetches
|
||
`GET /api/activity/{id}/geojson` and `GET /api/activity/{id}/timeseries`
|
||
on first open; both are Bearer-token-gated FastAPI endpoints
|
||
|
||
**Server additions:**
|
||
- `GET /api/activity/{id}/geojson` — reads `_merged/activities/{id}.geojson`
|
||
- `GET /api/activity/{id}/timeseries` — reads `activities/{id}.timeseries.json`
|
||
|
||
**App:**
|
||
- `@maplibre/maplibre-react-native`: route drawn as GeoJSON LineLayer over a
|
||
dark CartoDB base map; camera auto-fits track bounding box
|
||
- `react-native-svg`: elevation area chart from `elevation_m` + `t` arrays;
|
||
downsampled to ≤300 points; shows min/max elevation labels
|
||
|
||
**Done when:** Open any synced or locally imported activity — map and elevation
|
||
profile are visible within 1 s (local) or after one network round-trip (remote).
|
||
|
||
---
|
||
|
||
### Phase 5 — Polish *(ongoing)*
|
||
|
||
- **Offline map tiles** — bundle or download an MBTiles file for a region;
|
||
MapLibre supports offline tile sources
|
||
- **Batch import** — pick a folder (Strava export, Garmin bulk export); import all
|
||
FIT/GPX files found, with progress bar and per-file status
|
||
- **Share sheet** — Android intent filter for incoming `.fit`/`.gpx`/`.tcx` files
|
||
- **Re-extract** — button to re-run Pyodide extraction from the stored original file
|
||
|
||
---
|
||
|
||
## Out of scope for v1
|
||
|
||
- **Live activity recording** — GPS track + sensor data during a ride. This is the
|
||
eventual goal for complete platform independence but requires significant additional
|
||
work (background GPS, Bluetooth/ANT+ sensor integration, real-time display).
|
||
- **Editing activities** — read-only in v1; edits happen via the web interface.
|
||
- **Photo sync** — deferred.
|
||
|
||
---
|
||
|
||
## Future: toward full platform independence
|
||
|
||
Once live recording is implemented, the stack becomes:
|
||
|
||
```
|
||
Ride starts → Bincio records GPS + sensors (BLE power meter, HR strap, etc.)
|
||
Ride ends → Bincio extracts the activity locally (Pyodide or native)
|
||
→ Activity visible in the mobile feed immediately
|
||
→ Original FIT file saved on device
|
||
→ Optional: push to bincio.org for web access
|
||
```
|
||
|
||
At that point Garmin Connect, Hammerhead sync, and Strava become entirely optional.
|
||
The Karoo (or any Android head unit running the app) becomes a self-contained
|
||
training ecosystem.
|