feat(mobile/upload): upload_format setting + Option A local update from server response

Settings → Sync → Upload format: "Original file" (default) / "Extracted JSON".
- raw (default): reads original_path as base64, POSTs to /api/upload/raw; after
  success, overwrites local detail_json/timeseries_json/geojson/source_hash with
  the server's DEM-corrected extraction (Option A). Falls back to bas if the file
  is missing.
- bas: POSTs pre-extracted JSON to /api/upload/bas, faster, no DEM correction.

Switching modes is safe — the server deduplicates by activity id so a previous
raw upload will return status:"duplicate" on a subsequent bas attempt.
This commit is contained in:
Davide Scaini
2026-04-27 11:44:32 +02:00
parent 0d2176aef0
commit 93247d510f
3 changed files with 65 additions and 15 deletions
+18 -4
View File
@@ -15,14 +15,16 @@ export default function SettingsScreen() {
const storedHandle = useSetting('handle') ?? '';
const storedPath = useSetting('auto_import_path') ?? '';
const storedToken = useSetting('api_token');
const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full';
const storedSyncUpload = useSetting('sync_upload') === 'true';
const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full';
const storedSyncUpload = useSetting('sync_upload') === 'true';
const storedUploadFormat = (useSetting('upload_format') ?? 'raw') as 'raw' | 'bas';
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
const [handle, setHandle] = useState(storedHandle);
const [autoPath, setAutoPath] = useState(storedPath);
const [syncMode, setSyncMode] = useState(storedSyncMode);
const [syncUpload, setSyncUpload] = useState(storedSyncUpload);
const [syncMode, setSyncMode] = useState(storedSyncMode);
const [syncUpload, setSyncUpload] = useState(storedSyncUpload);
const [uploadFormat, setUploadFormat] = useState(storedUploadFormat);
const [saved, setSaved] = useState(false);
const theme = useTheme();
const { paletteKey: palette, setPaletteOverride } = usePaletteControl();
@@ -213,6 +215,18 @@ export default function SettingsScreen() {
? 'Local activities are uploaded to the instance during sync.'
: 'Local activities stay on device only.'}
</Text>
<Text style={[styles.subLabel, { borderTopWidth: 1, borderTopColor: '#27272a', paddingTop: 12 }]}>Upload format</Text>
<View style={styles.modeRow}>
<ModeButton label="Original file" active={uploadFormat === 'raw'} accent={theme.accent} dim={theme.dim}
onPress={() => { setUploadFormat('raw'); setSetting(db, 'upload_format', 'raw'); }} />
<ModeButton label="Extracted JSON" active={uploadFormat === 'bas'} accent={theme.accent} dim={theme.dim}
onPress={() => { setUploadFormat('bas'); setSetting(db, 'upload_format', 'bas'); }} />
</View>
<Text style={styles.hint}>
{uploadFormat === 'raw'
? 'Uploads the original FIT/GPX/TCX file. The server re-extracts it with DEM elevation correction and updates your local copy.'
: 'Uploads the pre-extracted JSON. Faster, but no DEM elevation correction.'}
</Text>
</Section>
<Section title="Palette">
+37 -3
View File
@@ -170,6 +170,7 @@ async function uploadLocalActivities(
FROM activities WHERE origin = 'local' AND synced_at IS NULL`,
);
const preferRaw = (await getSetting(db, 'upload_format') ?? 'raw') === 'raw';
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
let uploaded = 0;
let failed = 0;
@@ -182,9 +183,10 @@ async function uploadLocalActivities(
try {
let resp: Response;
// Prefer raw upload when the original FIT/GPX/TCX file is still on disk.
// The server re-extracts it with DEM elevation correction, producing better data.
const useRaw = row.original_path !== null &&
// When preferRaw is set and the original file is still on disk, send the raw
// bytes to /api/upload/raw so the server re-extracts with DEM elevation correction.
const useRaw = preferRaw &&
row.original_path !== null &&
(await FileSystem.getInfoAsync(row.original_path)).exists;
if (useRaw) {
@@ -211,6 +213,38 @@ async function uploadLocalActivities(
if (resp.ok) {
await db.runAsync(`UPDATE activities SET synced_at = ? WHERE id = ?`, [now, row.id]);
// Option A: after a raw upload, update local detail/timeseries/geojson with the
// server's DEM-corrected extraction so the app shows better elevation data.
if (useRaw) {
try {
const data = await resp.json() as {
id: string;
detail: object;
timeseries: object | null;
geojson: object | null;
source_hash: string;
};
if (data.id === row.id) {
await db.runAsync(
`UPDATE activities
SET detail_json = ?,
timeseries_json = COALESCE(?, timeseries_json),
geojson = COALESCE(?, geojson),
source_hash = ?
WHERE id = ?`,
[
JSON.stringify(data.detail),
data.timeseries ? JSON.stringify(data.timeseries) : null,
data.geojson ? JSON.stringify(data.geojson) : null,
data.source_hash,
row.id,
],
);
}
} catch {
// Non-fatal: synced_at is already set, local data stays as-is
}
}
uploaded++;
} else {
console.warn(`upload ${row.id}: HTTP ${resp.status}`);