feat(mobile): Karoo GPU crash fix, server-side extraction, upload fix, feed redesign
- Skip MapLibre on Android <29 (Karoo): SELinux denies kgsl-3d0 access from untrusted_app context, crashing the GPU driver on any OpenGL surface. Replace with SvgRouteView — equirectangular SVG route trace using react-native-svg, no native GL surface needed. - Add +/- zoom buttons to full-screen MapLibre map on modern devices via Camera ref and onRegionDidChange. - Skip PyodideWebView on Android <29: same GPU driver conflict; set _engineUnavailable at module init via API level gate (< 29). - Add engine_unavailable fast path in PyodideWebView: post message immediately if WebAssembly.Global is absent (Chrome <69) instead of attempting 30 MB Pyodide download. - Add server-side extraction fallback (extractServer.ts): when engine unavailable, POST raw file as base64 to /api/upload/raw; server runs full Python pipeline and returns extracted data. - Add /api/upload/raw endpoint in server.py. - Add pre-flight auth check (checkServerAuth) before batch import so an expired token errors immediately rather than after N files. - Fix uploadLocalActivities in sync.ts: was reading original_path as JSON (binary FIT file, always threw), silently skipping every upload. Now reads detail_json from DB directly. - Redesign Feed header: replace single Sync button with Upload / Download / Refresh. Pull-to-refresh and Refresh button are local-only. Auto-refresh on tab focus via useFocusEffect. - Replace ActivityIndicator with plain Text everywhere (native animation also crashes Karoo GPU driver). - Raise macOS open-file limit in dev_test.py to prevent EMFILE errors from Astro file watcher. - Document all Karoo hardware constraints in docs/mobile-app.md.
This commit is contained in:
@@ -539,6 +539,109 @@ async def upload_bas_activity(
|
||||
return JSONResponse({"ok": True, "id": activity_id, "status": "imported"})
|
||||
|
||||
|
||||
@app.post("/api/upload/raw")
|
||||
async def upload_raw_activity(
|
||||
request: Request,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Accept a raw FIT/GPX file (base64-encoded) from the mobile app, extract it
|
||||
server-side, store it in the user's activity library, and return the full
|
||||
extracted data so the mobile can cache it locally.
|
||||
|
||||
Used when the device WebView is too old to run Pyodide (e.g. Karoo / Chrome <69).
|
||||
|
||||
Body (JSON):
|
||||
filename – original filename (used only to determine file extension)
|
||||
base64 – base64-encoded raw file bytes
|
||||
|
||||
Auth: Authorization: Bearer <token>
|
||||
|
||||
Returns:
|
||||
{"ok": true, "id": "...", "detail": {...}, "timeseries": {...}|null,
|
||||
"geojson": {...}|null, "source_hash": "<sha256-hex>"}
|
||||
"""
|
||||
import base64 as _b64
|
||||
import hashlib
|
||||
|
||||
user = _require_auth(request, bincio_session)
|
||||
|
||||
body = await request.json()
|
||||
filename_hint: str = body.get("filename") or "activity.fit"
|
||||
b64: str = body.get("base64") or ""
|
||||
if not b64:
|
||||
raise HTTPException(400, "Missing base64 field")
|
||||
|
||||
try:
|
||||
raw = _b64.b64decode(b64)
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid base64 encoding")
|
||||
|
||||
source_hash = hashlib.sha256(raw).hexdigest()
|
||||
|
||||
suffix = Path(filename_hint).suffix or ".fit"
|
||||
tmp_in = Path(f"/tmp/bincio_raw_{uuid.uuid4()}{suffix}")
|
||||
tmp_out = Path(f"/tmp/bincio_out_{uuid.uuid4()}")
|
||||
try:
|
||||
tmp_in.write_bytes(raw)
|
||||
tmp_out.mkdir()
|
||||
|
||||
from bincio.extract.parsers.factory import parse_file
|
||||
from bincio.extract.metrics import compute
|
||||
from bincio.extract.writer import make_activity_id, write_activity
|
||||
from bincio.extract.timeseries import build_timeseries
|
||||
|
||||
activity = parse_file(tmp_in)
|
||||
metrics = compute(activity)
|
||||
write_activity(activity, metrics, tmp_out, privacy="public", rdp_epsilon=0.0001)
|
||||
act_id = make_activity_id(activity)
|
||||
|
||||
acts_tmp = tmp_out / "activities"
|
||||
detail_path = acts_tmp / f"{act_id}.json"
|
||||
ts_path = acts_tmp / f"{act_id}.timeseries.json"
|
||||
geojson_path = acts_tmp / f"{act_id}.geojson"
|
||||
|
||||
if not ts_path.exists():
|
||||
ts_data = build_timeseries(activity.points, activity.started_at, "public")
|
||||
if ts_data.get("t"):
|
||||
ts_path.write_text(json.dumps(ts_data))
|
||||
|
||||
detail = json.loads(detail_path.read_text())
|
||||
timeseries = json.loads(ts_path.read_text()) if ts_path.exists() else None
|
||||
geojson = json.loads(geojson_path.read_text()) if geojson_path.exists() else None
|
||||
|
||||
# Also store on the server so the activity appears in the user's feed.
|
||||
user_dir = _get_data_dir() / user.handle
|
||||
acts_dir = user_dir / "activities"
|
||||
acts_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = acts_dir / f"{act_id}.json"
|
||||
if not out.exists():
|
||||
out.write_text(json.dumps(detail, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
if timeseries and not (acts_dir / f"{act_id}.timeseries.json").exists():
|
||||
(acts_dir / f"{act_id}.timeseries.json").write_text(json.dumps(timeseries), encoding="utf-8")
|
||||
if geojson and not (acts_dir / f"{act_id}.geojson").exists():
|
||||
(acts_dir / f"{act_id}.geojson").write_text(json.dumps(geojson), encoding="utf-8")
|
||||
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(user_dir)
|
||||
|
||||
except Exception as exc:
|
||||
log.warning("upload/raw[%s]: extraction failed: %s", user.handle, exc)
|
||||
raise HTTPException(422, f"Could not extract activity: {exc}") from exc
|
||||
finally:
|
||||
tmp_in.unlink(missing_ok=True)
|
||||
shutil.rmtree(tmp_out, ignore_errors=True)
|
||||
|
||||
log.info("upload/raw[%s]: imported %s", user.handle, act_id)
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"id": act_id,
|
||||
"detail": detail,
|
||||
"timeseries": timeseries,
|
||||
"geojson": geojson,
|
||||
"source_hash": source_hash,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/wheel/version")
|
||||
async def wheel_version() -> JSONResponse:
|
||||
"""Public endpoint: current bincio wheel version for mobile app update checks."""
|
||||
|
||||
Reference in New Issue
Block a user