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:
+68
-2
@@ -487,6 +487,67 @@ async def get_activity_timeseries(
|
||||
raise HTTPException(404, "Timeseries not found")
|
||||
|
||||
|
||||
def _upsert_index_summary(user_dir: Path, activity_id: str, activity: dict, geojson: Optional[dict] = None) -> None:
|
||||
"""Add or update an activity summary in user_dir/index.json.
|
||||
|
||||
Called after writing BAS activity files so that merge_all can include the
|
||||
activity in year shards. Without this, uploaded activities exist on disk
|
||||
but never appear in the browser feed.
|
||||
"""
|
||||
# Build preview coords from geojson if available ([lat, lng] order)
|
||||
preview: Optional[list] = None
|
||||
if geojson:
|
||||
try:
|
||||
coords = geojson.get("geometry", {}).get("coordinates", [])
|
||||
if coords:
|
||||
step = max(1, len(coords) // 9)
|
||||
preview = [[c[1], c[0]] for c in coords[::step]][:9]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
has_track = (user_dir / "activities" / f"{activity_id}.geojson").exists()
|
||||
summary = {
|
||||
"id": activity_id,
|
||||
"title": activity.get("title", activity_id),
|
||||
"sport": activity.get("sport"),
|
||||
"sub_sport": activity.get("sub_sport"),
|
||||
"started_at": activity.get("started_at"),
|
||||
"distance_m": activity.get("distance_m"),
|
||||
"duration_s": activity.get("duration_s"),
|
||||
"moving_time_s": activity.get("moving_time_s"),
|
||||
"elevation_gain_m": activity.get("elevation_gain_m"),
|
||||
"avg_speed_kmh": activity.get("avg_speed_kmh"),
|
||||
"max_speed_kmh": activity.get("max_speed_kmh"),
|
||||
"avg_hr_bpm": activity.get("avg_hr_bpm"),
|
||||
"max_hr_bpm": activity.get("max_hr_bpm"),
|
||||
"avg_cadence_rpm": activity.get("avg_cadence_rpm"),
|
||||
"avg_power_w": activity.get("avg_power_w"),
|
||||
"mmp": activity.get("mmp"),
|
||||
"best_efforts": activity.get("best_efforts"),
|
||||
"best_climb_m": activity.get("best_climb_m"),
|
||||
"source": activity.get("source"),
|
||||
"privacy": activity.get("privacy", "public"),
|
||||
"detail_url": f"activities/{activity_id}.json",
|
||||
"track_url": f"activities/{activity_id}.geojson" if has_track else None,
|
||||
"preview_coords": preview,
|
||||
}
|
||||
|
||||
index_path = user_dir / "index.json"
|
||||
if index_path.exists():
|
||||
index_data = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
index_data = {
|
||||
"bas_version": "1.0",
|
||||
"owner": {"handle": user_dir.name},
|
||||
"generated_at": None,
|
||||
"activities": [],
|
||||
}
|
||||
existing = {a["id"]: a for a in index_data.get("activities", [])}
|
||||
existing[activity_id] = summary
|
||||
index_data["activities"] = sorted(existing.values(), key=lambda a: a.get("started_at", ""), reverse=True)
|
||||
index_path.write_text(json.dumps(index_data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
@app.post("/api/upload/bas")
|
||||
async def upload_bas_activity(
|
||||
request: Request,
|
||||
@@ -527,10 +588,13 @@ async def upload_bas_activity(
|
||||
if not ts_path.exists():
|
||||
ts_path.write_text(json.dumps(body["timeseries"], ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
if body.get("geojson"):
|
||||
geojson_body: Optional[dict] = body.get("geojson") or None
|
||||
if geojson_body:
|
||||
gj_path = acts_dir / f"{activity_id}.geojson"
|
||||
if not gj_path.exists():
|
||||
gj_path.write_text(json.dumps(body["geojson"], ensure_ascii=False), encoding="utf-8")
|
||||
gj_path.write_text(json.dumps(geojson_body, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
_upsert_index_summary(user_dir, activity_id, activity, geojson_body)
|
||||
|
||||
from bincio.render.merge import merge_all, write_combined_feed
|
||||
merge_all(user_dir)
|
||||
@@ -622,6 +686,8 @@ async def upload_raw_activity(
|
||||
if geojson and not (acts_dir / f"{act_id}.geojson").exists():
|
||||
(acts_dir / f"{act_id}.geojson").write_text(json.dumps(geojson), encoding="utf-8")
|
||||
|
||||
_upsert_index_summary(user_dir, act_id, detail, geojson)
|
||||
|
||||
from bincio.render.merge import merge_all, write_combined_feed
|
||||
merge_all(user_dir)
|
||||
write_combined_feed(_get_data_dir())
|
||||
|
||||
Reference in New Issue
Block a user