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:
Davide Scaini
2026-04-26 21:00:12 +02:00
parent 4cabbea0d4
commit cbe3e0eeaf
10 changed files with 760 additions and 156 deletions
+30 -1
View File
@@ -1,4 +1,5 @@
import { createRef } from 'react';
import { Platform } from 'react-native';
import type WebView from 'react-native-webview';
import type { WebViewMessageEvent } from 'react-native-webview';
@@ -25,11 +26,31 @@ let isExtracting = false;
// Engine readiness — tracked so callers can wait before batching files.
let _engineReady = false;
let _engineError: string | null = null;
// Android <29 (API 27 = Android 8.1, e.g. Karoo) ships with a system WebView
// (Chrome <69) that lacks WebAssembly.Global, so Pyodide cannot run. Mounting
// a WebView on those devices also causes GPU driver crashes (SurfaceView
// conflicts). Skip the engine entirely and route to server extraction instead.
let _engineUnavailable = Platform.OS === 'android' && (Platform.Version as number) < 29;
const _engineResolvers: Array<() => void> = [];
const _engineRejecters: Array<(e: Error) => void> = [];
// Init-phase progress listeners (messages sent before any extraction starts).
const _progressListeners = new Set<(msg: string) => void>();
export function onEngineProgress(cb: (msg: string) => void): () => void {
_progressListeners.add(cb);
return () => _progressListeners.delete(cb);
}
export function isEngineAvailable(): boolean | null {
// null = not yet determined; true = ready; false = unavailable
if (_engineReady) return true;
if (_engineUnavailable || _engineError) return false;
return null;
}
export function waitForEngine(timeoutMs = 300_000): Promise<void> {
if (_engineReady) return Promise.resolve();
if (_engineUnavailable) return Promise.reject(new Error('engine_unavailable'));
if (_engineError) return Promise.reject(new Error(_engineError));
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
@@ -52,6 +73,10 @@ export function handleWebViewMessage(e: WebViewMessageEvent): void {
_engineReady = true;
_engineResolvers.splice(0).forEach(fn => fn());
break;
case 'engine_unavailable':
_engineUnavailable = true;
_engineRejecters.splice(0).forEach(fn => fn(new Error('engine_unavailable')));
break;
case 'init_error':
_engineError = msg.message as string;
_engineRejecters.splice(0).forEach(fn => fn(new Error(_engineError!)));
@@ -75,7 +100,11 @@ export function handleWebViewMessage(e: WebViewMessageEvent): void {
}
break;
case 'progress':
p?.onStatus(msg.msg as string);
if (p) {
p.onStatus(msg.msg as string);
} else {
_progressListeners.forEach(fn => fn(msg.msg as string));
}
break;
}
}