diff --git a/docs/mobile-app.md b/docs/mobile-app.md index e5184f7..a4ce915 100644 --- a/docs/mobile-app.md +++ b/docs/mobile-app.md @@ -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**: ~5–8 s (Pyodide startup + package load) -- **Subsequent extractions (warm WebView)**: ~1–3 s per activity -- **Pyodide memory footprint**: ~100–150 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) | ~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 (many files at once), the WebView is kept warm across -extractions, making the per-file cost just the Python execution time (~0.5–1 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 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 --- activities table -id TEXT PRIMARY KEY, -- BAS ID: "2026-04-17T074238Z" -source_hash TEXT NOT NULL, -- SHA-256 of original 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 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 +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()) +); --- settings table -key TEXT PRIMARY KEY, -value TEXT NOT NULL +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.