From f8c126fbda2943cbe6a9b59d6146b63b3a1f08ce Mon Sep 17 00:00:00 2001 From: owain Date: Sun, 7 Jun 2026 00:40:55 +0100 Subject: [PATCH] Add configurable sync_lookback_days for Garmin Connect Users can now set how many days back the first sync fetches. -1 syncs all history back to 2010; any positive value sets a rolling window. Values over 365 show a rate-limit warning in the UI. The default remains 30 days. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/garmin_sync.py | 12 +++++++++--- backend/app/services/garmin_connect_sync.py | 13 +++++++++---- backend/app/workers/tasks.py | 3 ++- frontend/src/pages/ProfilePage.jsx | 13 +++++++++++-- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/backend/app/api/garmin_sync.py b/backend/app/api/garmin_sync.py index 84345d2..e7cee88 100644 --- a/backend/app/api/garmin_sync.py +++ b/backend/app/api/garmin_sync.py @@ -18,6 +18,7 @@ class GarminConfigIn(BaseModel): sync_enabled: bool = True sync_activities: bool = True sync_wellness: bool = True + sync_lookback_days: int = 30 # days to look back on first sync; -1 = all-time class GarminConfigOut(BaseModel): @@ -25,9 +26,10 @@ class GarminConfigOut(BaseModel): sync_enabled: bool sync_activities: bool sync_wellness: bool + sync_lookback_days: int last_sync_at: Optional[datetime] last_sync_status: Optional[str] - connected: bool # True when credentials exist + connected: bool class Config: from_attributes = True @@ -45,14 +47,15 @@ async def get_config( if not cfg: return GarminConfigOut( email="", sync_enabled=False, sync_activities=True, - sync_wellness=True, last_sync_at=None, last_sync_status=None, - connected=False, + sync_wellness=True, sync_lookback_days=30, + last_sync_at=None, last_sync_status=None, connected=False, ) return GarminConfigOut( email=cfg.email, sync_enabled=cfg.sync_enabled, sync_activities=cfg.sync_activities, sync_wellness=cfg.sync_wellness, + sync_lookback_days=cfg.sync_lookback_days if cfg.sync_lookback_days is not None else 30, last_sync_at=cfg.last_sync_at, last_sync_status=cfg.last_sync_status, connected=True, @@ -92,6 +95,7 @@ async def save_config( cfg.sync_enabled = body.sync_enabled cfg.sync_activities = body.sync_activities cfg.sync_wellness = body.sync_wellness + cfg.sync_lookback_days = body.sync_lookback_days cfg.last_sync_status = "Credentials updated" else: cfg = GarminConnectConfig( @@ -102,6 +106,7 @@ async def save_config( sync_enabled=body.sync_enabled, sync_activities=body.sync_activities, sync_wellness=body.sync_wellness, + sync_lookback_days=body.sync_lookback_days, last_sync_status="Connected", ) db.add(cfg) @@ -114,6 +119,7 @@ async def save_config( sync_enabled=cfg.sync_enabled, sync_activities=cfg.sync_activities, sync_wellness=cfg.sync_wellness, + sync_lookback_days=cfg.sync_lookback_days if cfg.sync_lookback_days is not None else 30, last_sync_at=cfg.last_sync_at, last_sync_status=cfg.last_sync_status, connected=True, diff --git a/backend/app/services/garmin_connect_sync.py b/backend/app/services/garmin_connect_sync.py index 04c3e05..5f6e009 100644 --- a/backend/app/services/garmin_connect_sync.py +++ b/backend/app/services/garmin_connect_sync.py @@ -67,12 +67,13 @@ def authenticate_garmin(email: str, password_enc: str, token_store: Optional[str # ── Activity sync ───────────────────────────────────────────────────────────── def sync_activities(garmin, user_id: int, since: Optional[datetime], - db, file_store_path: str) -> int: + db, file_store_path: str, lookback_days: int = 30) -> int: """ List activities from Garmin Connect, skip any already in the DB, download FIT ZIPs for new ones, and queue them for processing. - On first sync (since=None) fetches the full account history back to 2010. + On first sync (since=None) the start date is determined by lookback_days: + -1 → full history back to 2010; N → today minus N days. On incremental syncs fetches from one day before last_sync_at. Returns the number of new activities queued. """ @@ -81,8 +82,12 @@ def sync_activities(garmin, user_id: int, since: Optional[datetime], from app.models.user import Activity from sqlalchemy import select, func - # First sync: fetch everything; incremental: one day overlap to catch late uploads - start_date = (since - timedelta(days=1)).date() if since else date(2010, 1, 1) + if since: + start_date = (since - timedelta(days=1)).date() + elif lookback_days == -1: + start_date = date(2010, 1, 1) + else: + start_date = date.today() - timedelta(days=max(lookback_days, 1)) end_date = date.today() try: diff --git a/backend/app/workers/tasks.py b/backend/app/workers/tasks.py index da33b71..907df6d 100644 --- a/backend/app/workers/tasks.py +++ b/backend/app/workers/tasks.py @@ -499,7 +499,8 @@ def sync_garmin_connect_user(user_id: int): if cfg.sync_activities: try: activities_queued = sync_activities( - garmin, user_id, cfg.last_sync_at, db, settings.file_store_path + garmin, user_id, cfg.last_sync_at, db, settings.file_store_path, + lookback_days=cfg.sync_lookback_days if cfg.sync_lookback_days is not None else 30, ) except Exception as exc: errors.append(f"activities: {exc}") diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx index 43e0e20..d40e22f 100644 --- a/frontend/src/pages/ProfilePage.jsx +++ b/frontend/src/pages/ProfilePage.jsx @@ -115,7 +115,7 @@ export default function ProfilePage() { queryKey: ['garmin-config'], queryFn: () => api.get('/garmin-sync/config').then(r => r.data), }) - const [gcForm, setGcForm] = useState({ email: '', password: '', sync_enabled: true, sync_activities: true, sync_wellness: true }) + const [gcForm, setGcForm] = useState({ email: '', password: '', sync_enabled: true, sync_activities: true, sync_wellness: true, sync_lookback_days: 30 }) const [gcSaved, setGcSaved] = useState(false) const [gcError, setGcError] = useState('') const [gcSyncing, setGcSyncing] = useState(false) @@ -127,6 +127,7 @@ export default function ProfilePage() { sync_enabled: garminConfig.sync_enabled, sync_activities: garminConfig.sync_activities, sync_wellness: garminConfig.sync_wellness, + sync_lookback_days: garminConfig.sync_lookback_days ?? 30, })) } }, [garminConfig]) @@ -145,7 +146,7 @@ export default function ProfilePage() { mutationFn: () => api.delete('/garmin-sync/config'), onSuccess: () => { refetchGarmin() - setGcForm({ email: '', password: '', sync_enabled: true, sync_activities: true, sync_wellness: true }) + setGcForm({ email: '', password: '', sync_enabled: true, sync_activities: true, sync_wellness: true, sync_lookback_days: 30 }) }, }) const triggerGarminSync = async () => { @@ -345,6 +346,14 @@ export default function ProfilePage() { ))} + + + setGcForm(f => ({ ...f, sync_lookback_days: parseInt(e.target.value, 10) || 30 }))} /> + {gcForm.sync_lookback_days > 365 && gcForm.sync_lookback_days !== -1 && ( +

Warning: syncing more than 365 days at once may take a long time and could trigger Garmin rate limits.

+ )} +
{gcError &&

{gcError}

}