Add trend-range gating, vehicle filter, sync cancel, moving time, and UI fixes
Build and push images / validate (push) Successful in 9s
Build and push images / build-backend (push) Successful in 1m57s
Build and push images / build-worker (push) Successful in 50s
Build and push images / build-frontend (push) Successful in 24s

- Grey out trend ranges beyond available health history
- Reject implausibly fast (vehicle) activities on upload with feedback
- Add cancel button + cooperative cancellation for Garmin sync
- Show daily steps prominently on the dashboard
- Clear errors for malformed/empty upload ZIPs
- Snap-target dot when drawing a segment on the map
- Time-axis fallback for stationary/HIIT HR timelines; hide map when no GPS
- Parse and display moving time (timer) vs elapsed; backfill task
- Restyle SegmentsPanel like RouteLeaderboard; Laps/Routes/Segments on one row

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 19:41:56 +01:00
parent 057eb9391a
commit ec87f68729
17 changed files with 569 additions and 132 deletions
+27 -4
View File
@@ -115,14 +115,21 @@ async def upload_garmin_export(
extract_dir = dest_dir / f"garmin_{dest.stem}"
task_ids = []
with zipfile.ZipFile(dest) as zf:
extracted = _safe_extract(zf, extract_dir)
try:
with zipfile.ZipFile(dest) as zf:
extracted = _safe_extract(zf, extract_dir)
except zipfile.BadZipFile:
dest.unlink(missing_ok=True)
raise HTTPException(status_code=400, detail="Uploaded file is not a valid ZIP archive")
has_health = False
for path in extracted:
suffix = path.suffix.lower()
if suffix == ".fit":
task = process_activity_file.delay(str(path), current_user.id, "fit")
task_ids.append(task.id)
elif suffix == ".json":
has_health = True # Garmin wellness data is exported as JSON files
elif suffix == ".zip":
# Garmin exports nest activity FIT files inside sub-zips
# (e.g. DI-Connect-Uploaded-Files/UploadedFiles_*_Part*.zip)
@@ -137,6 +144,12 @@ async def upload_garmin_export(
task = process_activity_file.delay(str(np), current_user.id, "fit")
task_ids.append(task.id)
if not task_ids and not has_health:
raise HTTPException(
status_code=400,
detail="No fitness data found in this archive — make sure you uploaded your full Garmin Connect export ZIP",
)
# Queue health/wellness data extraction
health_task = process_garmin_health_zip.delay(str(dest), current_user.id)
@@ -163,8 +176,12 @@ async def upload_strava_export(
extract_dir = dest_dir / f"strava_{dest.stem}"
task_ids = []
with zipfile.ZipFile(dest) as zf:
extracted = _safe_extract(zf, extract_dir)
try:
with zipfile.ZipFile(dest) as zf:
extracted = _safe_extract(zf, extract_dir)
except zipfile.BadZipFile:
dest.unlink(missing_ok=True)
raise HTTPException(status_code=400, detail="Uploaded file is not a valid ZIP archive")
for path in extracted:
suffix = path.suffix.lower()
@@ -172,6 +189,12 @@ async def upload_strava_export(
task = process_activity_file.delay(str(path), current_user.id, suffix[1:])
task_ids.append(task.id)
if not task_ids:
raise HTTPException(
status_code=400,
detail="No activity files (.fit or .gpx) found in this Strava archive",
)
return {
"status": "queued",
"activity_tasks": len(task_ids),