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:
+43
-39
@@ -1,4 +1,3 @@
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import type { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { getSetting, upsertRemoteActivity } from './queries';
|
||||
|
||||
@@ -10,13 +9,17 @@ export type SyncResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
async function resolveCredentials(db: SQLiteDatabase): Promise<{ instanceUrl: string; token: string } | { error: string }> {
|
||||
const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, '');
|
||||
const token = await getSetting(db, 'api_token');
|
||||
if (!instanceUrl || !token) return { error: 'No instance configured — add one in Settings.' };
|
||||
return { instanceUrl, token };
|
||||
}
|
||||
|
||||
if (!instanceUrl || !token) {
|
||||
return { synced: 0, total: 0, error: 'No instance configured — add one in Settings.' };
|
||||
}
|
||||
export async function downloadFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
const creds = await resolveCredentials(db);
|
||||
if ('error' in creds) return { synced: 0, total: 0, error: creds.error };
|
||||
const { instanceUrl, token } = creds;
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
@@ -27,16 +30,11 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
return { synced: 0, total: 0, error: 'Could not reach instance — check your connection.' };
|
||||
}
|
||||
|
||||
if (resp.status === 401) {
|
||||
return { synced: 0, total: 0, error: 'Session expired — reconnect in Settings.' };
|
||||
}
|
||||
if (!resp.ok) {
|
||||
return { synced: 0, total: 0, error: `Server error (${resp.status})` };
|
||||
}
|
||||
if (resp.status === 401) return { synced: 0, total: 0, error: 'Session expired — reconnect in Settings.' };
|
||||
if (!resp.ok) return { synced: 0, total: 0, error: `Server error (${resp.status})` };
|
||||
|
||||
const data: { activities?: RemoteSummary[] } = await resp.json();
|
||||
const activities = data.activities ?? [];
|
||||
|
||||
const syncMode = (await getSetting(db, 'sync_mode')) ?? 'summaries';
|
||||
|
||||
let synced = 0;
|
||||
@@ -57,18 +55,9 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
if (changed) synced++;
|
||||
}
|
||||
|
||||
// Upload local activities to the server if enabled
|
||||
const uploadEnabled = (await getSetting(db, 'sync_upload')) === 'true';
|
||||
let uploaded = 0;
|
||||
if (uploadEnabled) {
|
||||
uploaded = await uploadLocalActivities(db, instanceUrl, token);
|
||||
}
|
||||
if (syncMode !== 'full') return { synced, total: activities.length };
|
||||
|
||||
if (syncMode !== 'full') {
|
||||
return { synced, total: activities.length, uploaded: uploaded || undefined };
|
||||
}
|
||||
|
||||
// Full mode: fetch geojson + timeseries for any activity missing them
|
||||
// Full mode: fetch geojson + timeseries for activities missing them
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
let fetched = 0;
|
||||
for (const a of activities) {
|
||||
@@ -94,7 +83,7 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
if (gj !== null || ts !== null) {
|
||||
await db.runAsync(
|
||||
`UPDATE activities SET
|
||||
geojson = COALESCE(geojson, ?),
|
||||
geojson = COALESCE(geojson, ?),
|
||||
timeseries_json = COALESCE(timeseries_json, ?)
|
||||
WHERE id = ? AND origin = 'remote'`,
|
||||
[gj, ts, a.id],
|
||||
@@ -103,7 +92,30 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
}
|
||||
}
|
||||
|
||||
return { synced, total: activities.length, fetched, uploaded: uploaded || undefined };
|
||||
return { synced, total: activities.length, fetched };
|
||||
}
|
||||
|
||||
export async function uploadFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
const creds = await resolveCredentials(db);
|
||||
if ('error' in creds) return { synced: 0, total: 0, error: creds.error };
|
||||
const { instanceUrl, token } = creds;
|
||||
|
||||
const uploaded = await uploadLocalActivities(db, instanceUrl, token);
|
||||
return { synced: 0, total: 0, uploaded };
|
||||
}
|
||||
|
||||
export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||
const dl = await downloadFeed(db);
|
||||
if (dl.error) return dl;
|
||||
|
||||
const uploadEnabled = (await getSetting(db, 'sync_upload')) === 'true';
|
||||
let uploaded = 0;
|
||||
if (uploadEnabled) {
|
||||
const ul = await uploadFeed(db);
|
||||
uploaded = ul.uploaded ?? 0;
|
||||
}
|
||||
|
||||
return { ...dl, uploaded: uploaded || undefined };
|
||||
}
|
||||
|
||||
async function uploadLocalActivities(
|
||||
@@ -111,8 +123,8 @@ async function uploadLocalActivities(
|
||||
instanceUrl: string,
|
||||
token: string,
|
||||
): Promise<number> {
|
||||
const rows = db.getAllSync<{ id: string; original_path: string | null; timeseries_json: string | null; geojson: string | null }>(
|
||||
`SELECT id, original_path, timeseries_json, geojson
|
||||
const rows = db.getAllSync<{ id: string; detail_json: string; timeseries_json: string | null; geojson: string | null }>(
|
||||
`SELECT id, detail_json, timeseries_json, geojson
|
||||
FROM activities WHERE origin = 'local' AND synced_at IS NULL`,
|
||||
);
|
||||
|
||||
@@ -122,14 +134,9 @@ async function uploadLocalActivities(
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
let activity: object | null = null;
|
||||
|
||||
if (row.original_path) {
|
||||
const text = await FileSystem.readAsStringAsync(row.original_path);
|
||||
activity = JSON.parse(text);
|
||||
}
|
||||
|
||||
if (!activity) continue;
|
||||
const detail = JSON.parse(row.detail_json);
|
||||
// /api/upload/bas expects { activity: { id, ...detail }, timeseries?, geojson? }
|
||||
const activity = { id: row.id, ...detail };
|
||||
|
||||
const body: Record<string, unknown> = { activity };
|
||||
if (row.timeseries_json) body.timeseries = JSON.parse(row.timeseries_json);
|
||||
@@ -142,10 +149,7 @@ async function uploadLocalActivities(
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
await db.runAsync(
|
||||
`UPDATE activities SET synced_at = ? WHERE id = ?`,
|
||||
[now, row.id],
|
||||
);
|
||||
await db.runAsync(`UPDATE activities SET synced_at = ? WHERE id = ?`, [now, row.id]);
|
||||
uploaded++;
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user