diff --git a/bincio/edit/server.py b/bincio/edit/server.py index 2f25567..c919bce 100644 --- a/bincio/edit/server.py +++ b/bincio/edit/server.py @@ -11,7 +11,7 @@ from typing import Any from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi.middleware.cors import CORSMiddleware 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 @@ -467,6 +467,37 @@ async def recalculate_elevation_hysteresis_endpoint(activity_id: str) -> JSONRes 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") async def upload_image(activity_id: str, file: UploadFile = File(...)) -> JSONResponse: dd = _get_data_dir() diff --git a/bincio/serve/server.py b/bincio/serve/server.py index ff78399..492e0c1 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -23,7 +23,7 @@ from typing import Any, Optional log = logging.getLogger("bincio.serve") 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.gzip import GZipMiddleware from fastapi.responses import JSONResponse @@ -448,9 +448,27 @@ async def wheel_version() -> JSONResponse: return JSONResponse({ "version": version, "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) async def login( login_req: LoginRequest, diff --git a/docs/mobile-app.md b/docs/mobile-app.md index 3f0a3a2..1f7cb08 100644 --- a/docs/mobile-app.md +++ b/docs/mobile-app.md @@ -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 -| Tool | Minimum version | Notes | +| Tool | Required for | Install | |---|---|---| -| Node.js | 18 | 20 LTS recommended — install via [nodejs.org](https://nodejs.org) or `nvm` | -| npm | ships with Node | | -| Expo Go app | latest | Install on your phone — scan the QR code to run the app instantly during development | -| Xcode | 15+ | **macOS only, iOS builds.** Install from the App Store, then `xcode-select --install` | -| Android Studio | latest | **Android builds / emulator.** Includes the SDK and `adb` | +| 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 Xcode or Android Studio to start. Expo Go lets you run the app -on your physical device by scanning a QR code — no native build required. +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 @@ -73,25 +106,153 @@ on your physical device by scanning a QR code — no native build required. bash mobile/setup.sh ``` -The script checks prerequisites, installs npm dependencies, and generates the -required Expo type declarations. It prints next steps when done. +The script checks Node, Android SDK, and Xcode availability; installs npm +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 cd mobile - -# Development server — scan QR with Expo Go on your phone npx expo start +``` -# Run on a connected Android device or emulator -npx expo run:android +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. -# Run on iOS simulator (macOS only) -npx expo run:ios +> **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`. -# 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 ``` --- diff --git a/mobile/assets/bincio.whl b/mobile/assets/bincio.whl new file mode 100644 index 0000000..18bdd2e Binary files /dev/null and b/mobile/assets/bincio.whl differ