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:
Davide Scaini
2026-04-24 11:01:24 +02:00
parent b37df88fe1
commit 02bb8a3dd7
4 changed files with 232 additions and 22 deletions
+32 -1
View File
@@ -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()
+19 -1
View File
@@ -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,