docs: complete mobile app plan — phased roadmap, Android/iOS divergences, data model

This commit is contained in:
Davide Scaini
2026-04-24 10:26:58 +02:00
parent 61479fe554
commit 565f5a3ff1
+317 -190
View File
@@ -2,10 +2,10 @@
## Vision
The long-term goal is full independence from Garmin Connect, Strava, 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 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:
@@ -17,6 +17,10 @@ The Bincio mobile app removes that dependency:
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
@@ -38,285 +42,408 @@ available, the app downloads a fresh copy of the extraction algorithm from binci
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 (bincio.org or
self-hosted) adds cloud backup, the web feed, and the ability to share activities.
The app never silently overwrites local data. Sync is user-initiated.
**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.
**Open format.** Activities are stored in the BAS schema the same JSON format the
server uses. Any tool in any language can read them.
---
## 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
Several pieces of the mobile app are already implemented or proven:
| 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 running in the browser. **This is the proof of concept for mobile extraction** — a hidden WebView in the app uses the exact same mechanism. |
| Bincio wheel | served at `/bincio-0.1.0-py3-none-any.whl` | The extraction code packaged as a pure-Python wheel. Already downloaded and run by the `/convert/` page. |
| Local activity storage | `site/src/pages/convert/` | IndexedDB + service worker in the web app. Proves the concept; the mobile app uses SQLite instead. |
| Content-addressed dedup | `bincio/extract/dedup.py` | `source_hash` (SHA-256 of raw file) prevents duplicates on upload |
| Sync-ready REST API | `bincio/serve/server.py` | Login, upload, activity detail, index.json — the sync primitives are already there |
| Settings persistence | `bincio/serve/db.py` | `settings` table (key/value) for instance URL, auth token, sync preferences |
| 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 choice
## Technology
### Cross-platform framework: Expo (React Native)
### Framework: Expo (React Native)
**Expo** (React Native + the Expo SDK) is the recommended platform:
- TypeScript-first, large ecosystem
- `expo-sqlite` (v2+) — fast on-device SQLite with WAL mode
- File picking from device storage: `expo-document-picker`
- Direct filesystem access (critical for Karoo): `expo-file-system`
- Maps: MapLibre React Native (`@maplibre/maplibre-react-native`)same tile
standard as the web app, self-hostable
- Background tasks: `expo-background-fetch` / `expo-task-manager`
- Expo Go for rapid development without a build step
- EAS Build for iOS and Android distribution
**Why not alternatives:**
| Option | Reason for skipping |
|---|---|
| Capacitor + Svelte | WebView performance is poor for map-heavy activity detail; same hidden-WebView trick for Pyodide applies either way |
| Flutter | Dart is a new language; no practical advantage for this use case |
| PWA | iOS limits background sync, local storage quotas, and filesystem access — not viable for an activity logger |
- 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
This is the core technical insight. The `/convert/` page already demonstrates that
the full Python extraction pipeline can run in a browser via **Pyodide** (CPython
compiled to WebAssembly). A React Native app can host a hidden `WebView` component
running the exact same code. No rewrite required.
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.
### How the /convert/ page does it today
### Package stack (proven in /convert/ today)
```
Browser tab
── Pyodide (CPython → WASM, ~30 MB)
├── lxml (pre-compiled in Pyodide — XML/GPX parsing)
├── fitdecode (pure Python — FIT parsing)
├── gpxpy (pure Python — GPX parsing)
├── pyyaml (pure Python)
└── bincio wheel (pure Python — metrics, hysteresis, writers)
fetched from: /bincio-0.1.0-py3-none-any.whl
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
```
All dependencies are either pre-compiled in Pyodide or **pure Python with no C
extensions**. This is the key: there is nothing to recompile for mobile.
Every dependency is either pre-compiled in Pyodide or **pure Python with no C
extensions**. Nothing needs recompilation for mobile.
### How the mobile app does it
### Data flow
```
React Native app
└── Hidden WebView (WKWebView on iOS, Chrome WebView on Android)
└── Same Pyodide environment as the /convert/ page
├── Pyodide runtime (cached on device after first download)
├── lxml, fitdecode, gpxpy, pyyaml (cached)
└── bincio wheel (fetched from bincio.org on startup / version check)
React Native
1. Read file bytes from device filesystem (expo-file-system)
2. postMessage({ type: 'extract', filename, bytes }) → hidden WebView
Data flow:
1. App reads FIT file bytes from device filesystem
2. Sends bytes to WebView via postMessage
3. WebView writes bytes to Pyodide's virtual FS
4. Python runs the extraction → BAS JSON dict
5. WebView sends JSON back via postMessage
6. App stores BAS JSON in SQLite, original file on disk
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.** The only network traffic is:
- Pyodide runtime (CDN or bundled, ~30 MB, cached)
- Common packages (CDN or bundled, cached)
- The bincio wheel from bincio.org (~50 KB, updated on version bump)
**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
### Algorithm updates without App Store releases
The bincio wheel is versioned and served from bincio.org. On app startup (or
periodically), the app checks the current wheel version:
The bincio wheel is versioned. On app startup the app calls:
```
GET https://bincio.org/bincio-latest.whl (or a version manifest endpoint)
GET /api/wheel/version → { "version": "0.2.1", "url": "/bincio-0.2.1-py3-none-any.whl" }
```
If a new version is available, the wheel is downloaded and cached. The next
extraction uses the updated algorithm. Improvements to hysteresis thresholds,
DEM correction, lap detection, or any other metric are live on all devices
within hours of deployment — **no App Store submission required**.
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
- **First extraction after install**: ~58 s (Pyodide startup + package load)
- **Subsequent extractions (warm WebView)**: ~13 s per activity
- **Pyodide memory footprint**: ~100150 MB RAM while active; the WebView can
be suspended between extractions
- **Wheel size**: the bincio extract code is ~50 KB; Pyodide + packages ~30 MB
(downloaded once, cached on device)
| Scenario | Time |
|---|---|
| First extraction (cold Pyodide + packages) | ~58 s |
| First extraction in session (warm WebView) | ~13 s |
| Subsequent extractions (warm WebView) | ~0.51 s |
| Pyodide RAM while active | ~100150 MB |
For batch import (many files at once), the WebView is kept warm across
extractions, making the per-file cost just the Python execution time (~0.51 s
per typical activity).
For batch import the WebView is kept alive across files; per-file cost drops to
the Python execution time only.
---
## Architecture
## Android vs iOS: platform divergences
```
Bincio Mobile
├── UI Layer (React Native / Expo)
│ ├── Feed screen — list of local activities, sorted by date
│ ├── Activity detail — map + elevation chart + stats
│ ├── Import screen — pick FIT/GPX/TCX from device or share sheet
│ ├── Sync screen — configure instance URL, push/pull
│ └── Settings screen — account, preferences, storage info
├── Extraction Engine (Pyodide in hidden WebView)
│ ├── WebView host — manages lifecycle, message passing
│ ├── Wheel cache — versioned bincio wheel stored on device
│ └── Python runtime — Pyodide + fitdecode + gpxpy + lxml
│ identical to the /convert/ page on the web
├── Local Store (expo-sqlite)
│ ├── activities — BAS detail JSON + indexed summary columns
│ ├── timeseries — 1 Hz arrays as JSON blob per activity
│ ├── geojson — simplified GPS track per activity
│ ├── originals — original file paths per activity
│ └── settings — instance_url, handle, auth_token, sync prefs
└── Sync Layer (optional)
├── Auth — POST /api/auth/login → Bearer token
├── Push — POST /api/upload (original file)
└── Pull — GET index.json + activity/{id}.json + timeseries
```
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 300600 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
-- activities table
CREATE TABLE activities (
id TEXT PRIMARY KEY, -- BAS ID: "2026-04-17T074238Z"
source_hash TEXT NOT NULL, -- SHA-256 of original file (dedup key)
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)
timeseries_json TEXT, -- 1 Hz arrays, loaded lazily
geojson TEXT, -- simplified GPS track
original_path TEXT NOT NULL, -- path to original file in app storage
synced_at INTEGER, -- unix timestamp of last push to remote
origin TEXT NOT NULL, -- "local" | "remote"
created_at INTEGER NOT NULL
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())
);
-- settings table
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
**Settings keys:**
| Key | Example value |
|---|---|
| `instance_url` | `https://bincio.org` |
| `handle` | `brutsalvadi` |
| `session_token` | `abc123…` |
| `last_sync_at` | `2026-04-24T10:00:00Z` |
| `wheel_version` | `0.1.0` |
| `auto_import_path` | `/sdcard/Karoo/Rides/` (Android only) |
---
## Karoo and Android-first devices
Devices like the **Karoo 2** run Android and write FIT files directly to the
filesystem (e.g. `/sdcard/Karoo/Rides/`). The app can monitor this directory and
auto-import new files as rides complete — no manual export step, no Hammerhead
cloud sync, no Garmin Connect, no Strava required.
On Karoo specifically:
- Install the Bincio Android APK directly (sideload or via a store).
- Configure `auto_import_path` to point at the Karoo's ride directory.
- When a new FIT file appears, the app imports it automatically (Pyodide
extraction), stores the original file, and shows the ride in the feed.
- When WiFi is available and an instance is configured, rides can be pushed to
the instance for web access and backup.
This makes Bincio a complete replacement for Hammerhead's own sync infrastructure
for users who want full control of their data.
| 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 server protocol needed.
Sync is a two-way hash-based diff. No custom protocol is needed beyond the
existing REST API.
### Push (local → server)
1. Fetch `{instance_url}/{handle}/index.json` to get remote activity IDs.
2. Find local activities where `synced_at IS NULL`.
3. For each unsynced activity, `POST /api/upload` with the original file.
4. On 200, set `synced_at = now()`.
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. Fetch `{instance_url}/{handle}/index.json` (and yearly shards).
2. Find remote IDs not in the local DB.
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 {instance_url}/activities/{id}.json``detail_json`
- `GET {instance_url}/activities/{id}.timeseries.json``timeseries_json`
- `GET {instance_url}/activities/{id}.geojson``geojson`
4. Insert with `origin = "remote"`, `synced_at = now()`.
- `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()`.
Note: pulled activities don't have a local original file. If re-extraction is
needed (e.g. for a DEM correction), the original must be uploaded to the instance
first so the server can serve it back.
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 once created. The `source_hash` prevents double-counting:
if the same file is imported on two devices before sync, whichever arrives at the
server first wins; the duplicate is rejected with 409.
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 multi-user server currently uses HTTP session cookies. For the mobile client,
a **Bearer token** is cleaner:
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
{ "handle": "…", "password": "…" }
→ { "token": "abc123…", "expires_at": "" }
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 subsequent requests.
`Authorization: Bearer abc123…` on all API requests.
---
## What is out of scope for v1
## Implementation plan
- **Live activity recording** — GPS + sensor recording during a ride. This is a
much harder problem (background GPS, Bluetooth/ANT+ sensors, real-time display)
and is the eventual goal for full platform independence.
- **Offline map tiles** — v1 requires network for map rendering.
### 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.*
**`mobile/` scaffold:**
- `expo init mobile --template expo-template-blank-typescript`
- 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 fields, saved to `settings` table
**Import screen (stub):**
- `expo-document-picker` for `.fit`, `.gpx`, `.tcx`, `.json` files
- If a `.json` file is picked: parse as BAS detail, insert into `activities` (no timeseries), show in feed
- This lets the feed work before Pyodide is wired up
**Feed screen:**
- List of activities from `activities` table, sorted by `started_at`
- Each card: sport icon, title, date, distance, elevation
**Server (one small addition):**
- `GET /api/wheel/version``{ "version": "0.1.0", "url": "/bincio-0.1.0-py3-none-any.whl" }`
- No auth required; the wheel itself is already public
**Done when:** App launches on a phone, user enters instance URL and handle in
Settings, picks a `.json` BAS file, sees it in the Feed.
---
### Phase 1 — Import via Pyodide
*Goal: pick a FIT/GPX/TCX file, extract it on-device in ~5 s, see the full ride
with map and chart.*
**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
- `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 in SQLite
- Copies original file to `{documentDirectory}/originals/{source_hash}.{ext}`
- Duplicate detection via `source_hash` before extraction
**Activity detail screen:**
- Stats grid: distance, moving time, elevation gain/loss, avg speed, avg HR, avg power
- Map: MapLibre React Native with the GeoJSON track overlaid
- Elevation chart: simple SVG line chart from timeseries data
**Done when:** Drop a FIT file from a Karoo onto the phone, see the full ride
stats, map, and elevation profile within ~5 s.
---
### 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 — Sync
*Goal: activities recorded on the phone appear on bincio.org after one tap.*
**Server additions:**
- `POST /api/auth/token` — password login returning a Bearer token (long-lived,
stored securely; complements existing cookie auth, does not replace it)
**App:**
- Login screen: instance URL + handle + password → stores token
- Sync screen: last sync time, unsynced count, **Push** and **Pull** buttons
- Push: iterates unsynced local activities, `POST /api/upload` with original file
- Pull: fetches `index.json`, downloads missing activities, inserts as `origin = 'remote'`
- Progress indicator per activity (useful for first sync with many files)
**Done when:** Tap **Push**, activities appear on bincio.org with correct stats.
---
### Phase 4 — 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** — on Android, intent filter for incoming `.fit`/`.gpx`/`.tcx`
files from other apps; on iOS, Share Extension already set up in Phase 2
- **Home screen widget** — last activity summary or weekly km total
- **Re-extract** — button in activity detail to re-run Pyodide extraction from
the stored original file (picks up algorithm improvements)
---
## 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.
- **Watch / ANT+ / Bluetooth sensors** — deferred.
- **Editing activities on mobile** — read-only in v1; edits happen on the web.
---
## Roadmap
## Future: toward full platform independence
| Phase | Scope |
|---|---|
| **0 — Foundation** | Expo project scaffold, SQLite store, settings screen, file picker, display a BAS JSON read from disk |
| **1 — Import** | Hidden WebView + Pyodide extraction, wheel download and caching, local feed, activity detail with map and chart, original file storage |
| **2 — Karoo integration** | Auto-import from a watched directory, Android-specific filesystem access |
| **3 — Sync** | Bearer token auth, push/pull sync with a Bincio instance |
| **4 — Polish** | Offline map tiles, share sheet, home screen widget, batch import performance |
| **Future** | Live recording, Bluetooth/ANT+ sensors, full Garmin/Wahoo/Hammerhead replacement |
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.