fix(mobile/upload): activities now appear in browser after upload; reconcile synced_at on fresh server

Three bugs fixed:
- /api/upload/bas and /api/upload/raw never updated user_dir/index.json, so
  merge_all couldn't include uploaded activities in year shards — they existed
  on disk but were invisible to the browser feed. Fixed by _upsert_index_summary()
  called before merge_all().
- Silent catch {} in uploadLocalActivities swallowed all per-activity errors;
  replaced with console.warn so failures are visible in Expo logs.
- After a server wipe, synced_at flags on the device caused "Nothing to upload"
  forever. uploadFeed() now reconciles against GET /api/feed at the start of each
  upload: local activities not found on the server get synced_at cleared.

Also: live upload progress ("Uploading N / M…"), failed count in result message,
onProgress callback on uploadFeed(), countPendingUploads() helper.
This commit is contained in:
Davide Scaini
2026-04-27 11:03:00 +02:00
parent b1cf18a2f0
commit 220efb0d05
4 changed files with 143 additions and 26 deletions
+16 -12
View File
@@ -695,23 +695,27 @@ Implemented in `mobile/db/sync.ts` → `syncFeed()`.
### Upload (local → server)
Implemented in `mobile/db/sync.ts` → `uploadLocalActivities()`. Enabled when
Implemented in `mobile/db/sync.ts` → `uploadFeed()` / `uploadLocalActivities()`. Enabled when
`sync_upload = "true"` in settings, or triggered explicitly via the ↑ Upload button.
1. Query `activities WHERE origin = 'local' AND synced_at IS NULL`.
2. For each: parse `detail_json` from the DB row and construct `{ id: row.id, ...detail }`.
3. `POST {instance_url}/api/upload/bas` with body `{ activity, timeseries?, geojson? }`.
4. On 200 (including `{ status: "duplicate" }`): set `synced_at = unixepoch()`.
1. **Reconcile** against the server: fetch `GET /api/feed` and compare its activity IDs against local rows where `synced_at IS NOT NULL`. Any local activity that is marked as synced but absent from the server (e.g. server was wiped) has its `synced_at` cleared so it re-enters the upload queue. This is best-effort — if the feed fetch fails, upload proceeds with whatever is currently queued.
2. Query `activities WHERE origin = 'local' AND synced_at IS NULL`.
3. For each: parse `detail_json` from the DB row and construct `{ id: row.id, ...detail }`.
4. `POST {instance_url}/api/upload/bas` with body `{ activity, timeseries?, geojson? }`.
5. On 200 (including `{ status: "duplicate" }`): set `synced_at = unixepoch()`. On error: log to console, count as failed, continue with the next activity.
**Note:** `original_path` is not used in upload. An earlier implementation tried to read
`original_path` as a JSON file, but `original_path` stores the path to the original binary
FIT/GPX/TCX file — `JSON.parse()` always throws, silently skipping every activity. The correct
approach is to use the already-extracted `detail_json` stored in SQLite.
The UI shows live progress ("Uploading N / M…") during the batch and reports failures separately ("X uploaded, Y failed").
The server endpoint (`bincio/serve/server.py` → `POST /api/upload/bas`) accepts
pre-extracted BAS JSON rather than raw FIT/GPX/TCX. It deduplicates by checking
if the activity file already exists, writes geojson and timeseries if provided, then
calls `merge_all()` to refresh the server's merged feed.
pre-extracted BAS JSON rather than raw FIT/GPX/TCX. It writes the activity file,
**updates `user_dir/index.json`** with a summary entry (so `merge_all` can include
the activity in year shards and the browser feed), writes geojson and timeseries if
provided, then calls `merge_all()` + `write_combined_feed()`.
> **Bug that was fixed:** earlier versions of both `/api/upload/bas` and `/api/upload/raw`
> wrote activity files to disk but never updated `user_dir/index.json`. Since `merge_all`
> builds year shards from the index, uploaded activities existed on disk but were invisible
> to the browser feed. Fixed by `_upsert_index_summary()` called before `merge_all()`.
### Conflict handling