feat: serve bincio wheel locally for mobile dev testing
- Add GET /api/wheel/download to serve/server.py and edit/server.py: serves dist/bincio-*.whl via FileResponse; in production nginx takes the request before FastAPI, so this is a no-op there but works locally - wheel_version response now includes api_url: "/api/wheel/download" alongside the nginx-served url field - Bundle mobile/assets/bincio.whl (built from dist/) as an offline fallback for Pyodide testing before the first instance sync - docs/mobile-app.md: document dev setup — bundled asset, local server endpoint, and how to refresh the bundle with uv build + cp
This commit is contained in:
+32
-1
@@ -11,7 +11,7 @@ from typing import Any
|
|||||||
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||||
|
|
||||||
from bincio.edit.ops import SPORTS, STAT_PANELS, VALID_ACTIVITY_ID
|
from bincio.edit.ops import SPORTS, STAT_PANELS, VALID_ACTIVITY_ID
|
||||||
|
|
||||||
@@ -467,6 +467,37 @@ async def recalculate_elevation_hysteresis_endpoint(activity_id: str) -> JSONRes
|
|||||||
raise HTTPException(422, str(e))
|
raise HTTPException(422, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/wheel/version")
|
||||||
|
async def wheel_version() -> JSONResponse:
|
||||||
|
"""Public endpoint: current bincio wheel version for mobile app update checks."""
|
||||||
|
import importlib.metadata
|
||||||
|
try:
|
||||||
|
version = importlib.metadata.version("bincio")
|
||||||
|
except importlib.metadata.PackageNotFoundError:
|
||||||
|
version = "0.1.0"
|
||||||
|
return JSONResponse({
|
||||||
|
"version": version,
|
||||||
|
"url": f"/bincio-{version}-py3-none-any.whl",
|
||||||
|
"api_url": "/api/wheel/download",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/wheel/download")
|
||||||
|
async def wheel_download() -> FileResponse:
|
||||||
|
"""Serve the bincio wheel directly (used locally; in prod nginx serves /bincio-*.whl)."""
|
||||||
|
import importlib.metadata
|
||||||
|
try:
|
||||||
|
version = importlib.metadata.version("bincio")
|
||||||
|
except importlib.metadata.PackageNotFoundError:
|
||||||
|
version = "0.1.0"
|
||||||
|
wheel_name = f"bincio-{version}-py3-none-any.whl"
|
||||||
|
dist_dir = Path(__file__).parent.parent.parent / "dist"
|
||||||
|
wheel_path = dist_dir / wheel_name
|
||||||
|
if not wheel_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"{wheel_name} not found in dist/")
|
||||||
|
return FileResponse(wheel_path, media_type="application/zip", filename=wheel_name)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/activity/{activity_id}/images")
|
@app.post("/api/activity/{activity_id}/images")
|
||||||
async def upload_image(activity_id: str, file: UploadFile = File(...)) -> JSONResponse:
|
async def upload_image(activity_id: str, file: UploadFile = File(...)) -> JSONResponse:
|
||||||
dd = _get_data_dir()
|
dd = _get_data_dir()
|
||||||
|
|||||||
+19
-1
@@ -23,7 +23,7 @@ from typing import Any, Optional
|
|||||||
log = logging.getLogger("bincio.serve")
|
log = logging.getLogger("bincio.serve")
|
||||||
|
|
||||||
from fastapi import Cookie, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
|
from fastapi import Cookie, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
|
||||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -448,9 +448,27 @@ async def wheel_version() -> JSONResponse:
|
|||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"version": version,
|
"version": version,
|
||||||
"url": f"/bincio-{version}-py3-none-any.whl",
|
"url": f"/bincio-{version}-py3-none-any.whl",
|
||||||
|
"api_url": f"/api/wheel/download",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/wheel/download")
|
||||||
|
async def wheel_download() -> FileResponse:
|
||||||
|
"""Serve the bincio wheel directly (used locally; in prod nginx serves /bincio-*.whl)."""
|
||||||
|
import importlib.metadata
|
||||||
|
try:
|
||||||
|
version = importlib.metadata.version("bincio")
|
||||||
|
except importlib.metadata.PackageNotFoundError:
|
||||||
|
version = "0.1.0"
|
||||||
|
wheel_name = f"bincio-{version}-py3-none-any.whl"
|
||||||
|
# Look in dist/ relative to repo root (two levels up from this file)
|
||||||
|
dist_dir = Path(__file__).parent.parent.parent / "dist"
|
||||||
|
wheel_path = dist_dir / wheel_name
|
||||||
|
if not wheel_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"{wheel_name} not found in dist/")
|
||||||
|
return FileResponse(wheel_path, media_type="application/zip", filename=wheel_name)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/auth/login", response_model=LoginResponse)
|
@app.post("/api/auth/login", response_model=LoginResponse)
|
||||||
async def login(
|
async def login(
|
||||||
login_req: LoginRequest,
|
login_req: LoginRequest,
|
||||||
|
|||||||
+181
-20
@@ -51,20 +51,53 @@ server uses. Any tool in any language can read them.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup
|
## 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** only uses built-in Expo modules — Expo Go works. **Phase 1** (Pyodide)
|
||||||
|
requires a Development Build because `react-native-webview` is a native module.
|
||||||
|
|
||||||
|
The recommended setup from the start is a Development Build so you never hit a wall
|
||||||
|
mid-phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
| Tool | Minimum version | Notes |
|
| Tool | Required for | Install |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Node.js | 18 | 20 LTS recommended — install via [nodejs.org](https://nodejs.org) or `nvm` |
|
| Node.js 20 LTS | everything | [nodejs.org](https://nodejs.org) or `nvm install 20` |
|
||||||
| npm | ships with Node | |
|
| npm | everything | ships with Node |
|
||||||
| Expo Go app | latest | Install on your phone — scan the QR code to run the app instantly during development |
|
| Android Studio | Android dev build / emulator | [developer.android.com/studio](https://developer.android.com/studio) |
|
||||||
| Xcode | 15+ | **macOS only, iOS builds.** Install from the App Store, then `xcode-select --install` |
|
| Xcode 15+ | iOS only, macOS only | App Store → `xcode-select --install` |
|
||||||
| Android Studio | latest | **Android builds / emulator.** Includes the SDK and `adb` |
|
| EAS CLI | cloud builds (optional) | `npm install -g eas-cli` |
|
||||||
|
|
||||||
You do **not** need Xcode or Android Studio to start. Expo Go lets you run the app
|
You do **not** need a physical Android device to start. The Android emulator
|
||||||
on your physical device by scanning a QR code — no native build required.
|
(AVD Manager inside Android Studio) works fine for development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### First-time setup
|
### First-time setup
|
||||||
|
|
||||||
@@ -73,25 +106,153 @@ on your physical device by scanning a QR code — no native build required.
|
|||||||
bash mobile/setup.sh
|
bash mobile/setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script checks prerequisites, installs npm dependencies, and generates the
|
The script checks Node, Android SDK, and Xcode availability; installs npm
|
||||||
required Expo type declarations. It prints next steps when done.
|
dependencies; and generates the required Expo type declarations.
|
||||||
|
|
||||||
### Running the app
|
---
|
||||||
|
|
||||||
|
### Phase 0 — Expo Go (quickest start)
|
||||||
|
|
||||||
|
Since Phase 0 uses only built-in Expo modules, you can start with Expo Go:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd mobile
|
cd mobile
|
||||||
|
|
||||||
# Development server — scan QR with Expo Go on your phone
|
|
||||||
npx expo start
|
npx expo start
|
||||||
|
```
|
||||||
|
|
||||||
# Run on a connected Android device or emulator
|
1. Install **Expo Go** on your Android phone from the Play Store.
|
||||||
npx expo run:android
|
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.
|
||||||
|
|
||||||
# Run on iOS simulator (macOS only)
|
> **Limitation**: once you add the Pyodide WebView in Phase 1, you must switch to
|
||||||
npx expo run:ios
|
> a Development Build. Expo Go will show an error for `react-native-webview`.
|
||||||
|
|
||||||
# Build a standalone APK for Karoo sideloading
|
---
|
||||||
npx eas build -p android --profile preview
|
|
||||||
|
### 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: EAS Build (cloud, no Android Studio required)
|
||||||
|
|
||||||
|
EAS (Expo Application Services) builds the APK in the cloud. You get a download
|
||||||
|
link; install it on your device once.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g eas-cli
|
||||||
|
eas login # Expo account needed
|
||||||
|
eas build -p android --profile development
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user