feat(mobile/upload): send original FIT file via /api/upload/raw when available

When original_path is set (i.e. the original FIT/GPX/TCX is still on disk),
upload via POST /api/upload/raw { filename, base64 } so the server re-extracts
with DEM elevation correction.  Falls back to /api/upload/bas (pre-extracted
BAS JSON) when original_path is null or the file has been deleted.
This commit is contained in:
Davide Scaini
2026-04-27 11:18:29 +02:00
parent cfb3ba5871
commit 0d2176aef0
+37 -14
View File
@@ -1,3 +1,4 @@
import * as FileSystem from 'expo-file-system/legacy';
import type { SQLiteDatabase } from 'expo-sqlite'; import type { SQLiteDatabase } from 'expo-sqlite';
import { getSetting, upsertRemoteActivity } from './queries'; import { getSetting, upsertRemoteActivity } from './queries';
@@ -158,8 +159,14 @@ async function uploadLocalActivities(
token: string, token: string,
onProgress?: (n: number, total: number) => void, onProgress?: (n: number, total: number) => void,
): Promise<{ uploaded: number; failed: number }> { ): Promise<{ uploaded: number; failed: number }> {
const rows = db.getAllSync<{ id: string; detail_json: string; timeseries_json: string | null; geojson: string | null }>( const rows = db.getAllSync<{
`SELECT id, detail_json, timeseries_json, geojson id: string;
detail_json: string;
timeseries_json: string | null;
geojson: string | null;
original_path: string | null;
}>(
`SELECT id, detail_json, timeseries_json, geojson, original_path
FROM activities WHERE origin = 'local' AND synced_at IS NULL`, FROM activities WHERE origin = 'local' AND synced_at IS NULL`,
); );
@@ -173,28 +180,44 @@ async function uploadLocalActivities(
const row = rows[i]; const row = rows[i];
onProgress?.(i + 1, total); onProgress?.(i + 1, total);
try { try {
const detail = JSON.parse(row.detail_json); let resp: Response;
const activity = { id: row.id, ...detail };
const body: Record<string, unknown> = { activity }; // Prefer raw upload when the original FIT/GPX/TCX file is still on disk.
if (row.timeseries_json) body.timeseries = JSON.parse(row.timeseries_json); // The server re-extracts it with DEM elevation correction, producing better data.
if (row.geojson) body.geojson = JSON.parse(row.geojson); const useRaw = row.original_path !== null &&
(await FileSystem.getInfoAsync(row.original_path)).exists;
const resp = await fetch(`${instanceUrl}/api/upload/bas`, { if (useRaw) {
method: 'POST', const filename = row.original_path!.split('/').pop() ?? 'activity.fit';
headers, const base64 = await FileSystem.readAsStringAsync(row.original_path!, {
body: JSON.stringify(body), encoding: FileSystem.EncodingType.Base64,
}); });
resp = await fetch(`${instanceUrl}/api/upload/raw`, {
method: 'POST',
headers,
body: JSON.stringify({ filename, base64 }),
});
} else {
const detail = JSON.parse(row.detail_json);
const body: Record<string, unknown> = { activity: { id: row.id, ...detail } };
if (row.timeseries_json) body.timeseries = JSON.parse(row.timeseries_json);
if (row.geojson) body.geojson = JSON.parse(row.geojson);
resp = await fetch(`${instanceUrl}/api/upload/bas`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
}
if (resp.ok) { 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++; uploaded++;
} else { } else {
console.warn(`upload/bas ${row.id}: HTTP ${resp.status}`); console.warn(`upload ${row.id}: HTTP ${resp.status}`);
failed++; failed++;
} }
} catch (err) { } catch (err) {
console.warn(`upload/bas ${row.id}:`, err); console.warn(`upload ${row.id}:`, err);
failed++; failed++;
} }
} }