feat: Phase 4 — MapLibre route map + SVG elevation chart on activity screen
- 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)
This commit is contained in:
+85
-65
@@ -76,11 +76,13 @@ instantly via Metro (the JS bundler) — no rebuild needed.
|
||||
| Code changes | instant (Metro) | instant (Metro) |
|
||||
| Native changes | need new Expo Go release | rebuild APK |
|
||||
|
||||
**Phase 0** only uses built-in Expo modules — Expo Go works. **Phase 1** (Pyodide)
|
||||
requires a Development Build because `react-native-webview` is a native module.
|
||||
**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 recommended setup from the start is a Development Build so you never hit a wall
|
||||
mid-phase.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -147,25 +149,17 @@ 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: EAS Build (cloud, no Android Studio required)
|
||||
#### Option B: local EAS build (no Android Studio, no external cloud)
|
||||
|
||||
EAS (Expo Application Services) builds the APK in the cloud. You get a download
|
||||
link; install it on your device once.
|
||||
`eas build --local` runs the entire build pipeline on your own machine (or VPS):
|
||||
|
||||
```bash
|
||||
npm install -g eas-cli
|
||||
eas login # Expo account needed
|
||||
eas build -p android --profile development
|
||||
eas build -p android --profile development --local
|
||||
```
|
||||
|
||||
After install, start Metro locally:
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
npx expo start --dev-client
|
||||
```
|
||||
|
||||
Shake the device to open the dev menu and enter the Metro URL if needed.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -520,59 +514,60 @@ The token is stored in the `settings` table and sent as
|
||||
|
||||
## Implementation plan
|
||||
|
||||
### Phase 0 — Foundation
|
||||
### 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
|
||||
- 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)
|
||||
|
||||
**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.
|
||||
**Done when:** App launches, user 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.*
|
||||
### 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
|
||||
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 in SQLite
|
||||
- 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
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -596,35 +591,60 @@ 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.*
|
||||
### Phase 3 — Push sync
|
||||
*Goal: locally imported activities appear on the remote instance 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)
|
||||
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:**
|
||||
- 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)
|
||||
- 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**, activities appear on bincio.org with correct stats.
|
||||
**Done when:** Tap **Push**, locally imported activities appear on bincio.org.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — Polish *(ongoing)*
|
||||
### 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** — 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)
|
||||
- **Share sheet** — Android intent filter for incoming `.fit`/`.gpx`/`.tcx` files
|
||||
- **Re-extract** — button to re-run Pyodide extraction from the stored original file
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user