Fix Garmin stats sync, add route merge/map/links, fix PR constraint
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 7s
Build and push images / build-frontend (push) Successful in 10s

Garmin health: fix display_name=None when using stored OAuth tokens.
authenticate_garmin() now calls login(tokenstore=...) instead of
garth.loads() directly, so display_name is populated and get_user_summary
works. Also add avg_hr_day / max_hr_day from stats response.

Routes: add merge endpoint (POST /{id}/merge/{source}), delete endpoint.
Routes page: polyline SVG mini-map on each route card, merge UI with
confirmation, activity rows are now Links to the activity detail page.

Personal records: replace all-columns unique constraint with a partial
index (unique on current records only) to stop UniqueViolation crashes
when parallel workers deactivate the same PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 01:25:01 +01:00
parent edeb3ccece
commit 22b41109f5
4 changed files with 238 additions and 50 deletions
+8 -4
View File
@@ -46,15 +46,17 @@ def authenticate_garmin(email: str, password_enc: str, token_store: Optional[str
"""
import garminconnect
# Try stored OAuth token first (garth auto-refreshes access token on use)
# Try stored OAuth token first.
# Must call login(tokenstore=...) rather than garth.loads() directly so that
# garmin.display_name is populated — it's required by get_user_summary() and
# several other endpoints. Without it every stats call silently returns None.
if token_store:
try:
garmin = garminconnect.Garmin(
email=email, password=decrypt_password(password_enc)
)
garmin.garth.loads(token_store)
garmin.get_full_name() # lightweight request; triggers refresh if needed
return garmin, None # tokens still valid
garmin.login(tokenstore=token_store)
return garmin, None
except Exception as exc:
logger.info("Garmin token invalid (%s), re-authenticating", exc)
@@ -234,6 +236,8 @@ def _parse_day(stats, sleep_data, hrv_data) -> dict:
if stats:
_set(row, "resting_hr", stats.get("restingHeartRate"))
_set(row, "avg_hr_day", stats.get("averageHeartRate"))
_set(row, "max_hr_day", stats.get("maxHeartRate"))
_set(row, "steps", stats.get("totalSteps"))
_set(row, "floors_climbed", stats.get("floorsAscended"))
_set(row, "avg_stress", stats.get("averageStressLevel"))