diff --git a/backend/app/services/garmin_connect_sync.py b/backend/app/services/garmin_connect_sync.py index cb2e847..4e409a6 100644 --- a/backend/app/services/garmin_connect_sync.py +++ b/backend/app/services/garmin_connect_sync.py @@ -133,18 +133,24 @@ def sync_activities(garmin, user_id: int, since: Optional[datetime], continue # Slow-path dedup: activity imported via bulk export (no garmin_activity_id). - # Check by start_time; stamp the ID so future syncs skip it in the fast path. - act_start_str = act.get("startTimeLocal") or act.get("startTimeGMT") or "" + # Match on the actual start instant (not just the date — two activities on + # the same day are distinct), comparing GMT-to-GMT since FIT start_times are + # stored in UTC. A small window absorbs sub-second/rounding differences. + act_start_str = act.get("startTimeGMT") or "" if act_start_str: try: - from datetime import datetime as _dt + from datetime import datetime as _dt, timezone as _tz act_start = _dt.fromisoformat(act_start_str.replace("Z", "+00:00")) + if act_start.tzinfo is None: + act_start = act_start.replace(tzinfo=_tz.utc) # startTimeGMT is UTC + window = timedelta(minutes=5) time_match = db.execute( select(Activity).where( Activity.user_id == user_id, - func.date(Activity.start_time) == act_start.date(), + Activity.start_time >= act_start - window, + Activity.start_time <= act_start + window, ) - ).scalar_one_or_none() + ).scalars().first() if time_match: if not time_match.garmin_activity_id: time_match.garmin_activity_id = garmin_id diff --git a/milevault_export/backend/app/services/garmin_connect_sync.py b/milevault_export/backend/app/services/garmin_connect_sync.py index 8a2ea8f..b32ce03 100644 --- a/milevault_export/backend/app/services/garmin_connect_sync.py +++ b/milevault_export/backend/app/services/garmin_connect_sync.py @@ -127,18 +127,24 @@ def sync_activities(garmin, user_id: int, since: Optional[datetime], continue # Slow-path dedup: activity imported via bulk export (no garmin_activity_id). - # Check by start_time; stamp the ID so future syncs skip it in the fast path. - act_start_str = act.get("startTimeLocal") or act.get("startTimeGMT") or "" + # Match on the actual start instant (not just the date — two activities on + # the same day are distinct), comparing GMT-to-GMT since FIT start_times are + # stored in UTC. A small window absorbs sub-second/rounding differences. + act_start_str = act.get("startTimeGMT") or "" if act_start_str: try: - from datetime import datetime as _dt + from datetime import datetime as _dt, timezone as _tz act_start = _dt.fromisoformat(act_start_str.replace("Z", "+00:00")) + if act_start.tzinfo is None: + act_start = act_start.replace(tzinfo=_tz.utc) # startTimeGMT is UTC + window = timedelta(minutes=5) time_match = db.execute( select(Activity).where( Activity.user_id == user_id, - func.date(Activity.start_time) == act_start.date(), + Activity.start_time >= act_start - window, + Activity.start_time <= act_start + window, ) - ).scalar_one_or_none() + ).scalars().first() if time_match: if not time_match.garmin_activity_id: time_match.garmin_activity_id = garmin_id