Add configurable sync_lookback_days for Garmin Connect
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 10s

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 00:40:55 +01:00
parent 335bd0a053
commit f8c126fbda
4 changed files with 31 additions and 10 deletions
+9 -3
View File
@@ -18,6 +18,7 @@ class GarminConfigIn(BaseModel):
sync_enabled: bool = True sync_enabled: bool = True
sync_activities: bool = True sync_activities: bool = True
sync_wellness: bool = True sync_wellness: bool = True
sync_lookback_days: int = 30 # days to look back on first sync; -1 = all-time
class GarminConfigOut(BaseModel): class GarminConfigOut(BaseModel):
@@ -25,9 +26,10 @@ class GarminConfigOut(BaseModel):
sync_enabled: bool sync_enabled: bool
sync_activities: bool sync_activities: bool
sync_wellness: bool sync_wellness: bool
sync_lookback_days: int
last_sync_at: Optional[datetime] last_sync_at: Optional[datetime]
last_sync_status: Optional[str] last_sync_status: Optional[str]
connected: bool # True when credentials exist connected: bool
class Config: class Config:
from_attributes = True from_attributes = True
@@ -45,14 +47,15 @@ async def get_config(
if not cfg: if not cfg:
return GarminConfigOut( return GarminConfigOut(
email="", sync_enabled=False, sync_activities=True, email="", sync_enabled=False, sync_activities=True,
sync_wellness=True, last_sync_at=None, last_sync_status=None, sync_wellness=True, sync_lookback_days=30,
connected=False, last_sync_at=None, last_sync_status=None, connected=False,
) )
return GarminConfigOut( return GarminConfigOut(
email=cfg.email, email=cfg.email,
sync_enabled=cfg.sync_enabled, sync_enabled=cfg.sync_enabled,
sync_activities=cfg.sync_activities, sync_activities=cfg.sync_activities,
sync_wellness=cfg.sync_wellness, 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_at=cfg.last_sync_at,
last_sync_status=cfg.last_sync_status, last_sync_status=cfg.last_sync_status,
connected=True, connected=True,
@@ -92,6 +95,7 @@ async def save_config(
cfg.sync_enabled = body.sync_enabled cfg.sync_enabled = body.sync_enabled
cfg.sync_activities = body.sync_activities cfg.sync_activities = body.sync_activities
cfg.sync_wellness = body.sync_wellness cfg.sync_wellness = body.sync_wellness
cfg.sync_lookback_days = body.sync_lookback_days
cfg.last_sync_status = "Credentials updated" cfg.last_sync_status = "Credentials updated"
else: else:
cfg = GarminConnectConfig( cfg = GarminConnectConfig(
@@ -102,6 +106,7 @@ async def save_config(
sync_enabled=body.sync_enabled, sync_enabled=body.sync_enabled,
sync_activities=body.sync_activities, sync_activities=body.sync_activities,
sync_wellness=body.sync_wellness, sync_wellness=body.sync_wellness,
sync_lookback_days=body.sync_lookback_days,
last_sync_status="Connected", last_sync_status="Connected",
) )
db.add(cfg) db.add(cfg)
@@ -114,6 +119,7 @@ async def save_config(
sync_enabled=cfg.sync_enabled, sync_enabled=cfg.sync_enabled,
sync_activities=cfg.sync_activities, sync_activities=cfg.sync_activities,
sync_wellness=cfg.sync_wellness, 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_at=cfg.last_sync_at,
last_sync_status=cfg.last_sync_status, last_sync_status=cfg.last_sync_status,
connected=True, connected=True,
+9 -4
View File
@@ -67,12 +67,13 @@ def authenticate_garmin(email: str, password_enc: str, token_store: Optional[str
# ── Activity sync ───────────────────────────────────────────────────────────── # ── Activity sync ─────────────────────────────────────────────────────────────
def sync_activities(garmin, user_id: int, since: Optional[datetime], 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 List activities from Garmin Connect, skip any already in the DB, download
FIT ZIPs for new ones, and queue them for processing. 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. On incremental syncs fetches from one day before last_sync_at.
Returns the number of new activities queued. 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 app.models.user import Activity
from sqlalchemy import select, func from sqlalchemy import select, func
# First sync: fetch everything; incremental: one day overlap to catch late uploads if since:
start_date = (since - timedelta(days=1)).date() if since else date(2010, 1, 1) 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() end_date = date.today()
try: try:
+2 -1
View File
@@ -499,7 +499,8 @@ def sync_garmin_connect_user(user_id: int):
if cfg.sync_activities: if cfg.sync_activities:
try: try:
activities_queued = sync_activities( 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: except Exception as exc:
errors.append(f"activities: {exc}") errors.append(f"activities: {exc}")
+11 -2
View File
@@ -115,7 +115,7 @@ export default function ProfilePage() {
queryKey: ['garmin-config'], queryKey: ['garmin-config'],
queryFn: () => api.get('/garmin-sync/config').then(r => r.data), 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 [gcSaved, setGcSaved] = useState(false)
const [gcError, setGcError] = useState('') const [gcError, setGcError] = useState('')
const [gcSyncing, setGcSyncing] = useState(false) const [gcSyncing, setGcSyncing] = useState(false)
@@ -127,6 +127,7 @@ export default function ProfilePage() {
sync_enabled: garminConfig.sync_enabled, sync_enabled: garminConfig.sync_enabled,
sync_activities: garminConfig.sync_activities, sync_activities: garminConfig.sync_activities,
sync_wellness: garminConfig.sync_wellness, sync_wellness: garminConfig.sync_wellness,
sync_lookback_days: garminConfig.sync_lookback_days ?? 30,
})) }))
} }
}, [garminConfig]) }, [garminConfig])
@@ -145,7 +146,7 @@ export default function ProfilePage() {
mutationFn: () => api.delete('/garmin-sync/config'), mutationFn: () => api.delete('/garmin-sync/config'),
onSuccess: () => { onSuccess: () => {
refetchGarmin() 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 () => { const triggerGarminSync = async () => {
@@ -345,6 +346,14 @@ export default function ProfilePage() {
</label> </label>
))} ))}
</div> </div>
<Field label="Sync lookback days" hint="-1 syncs all available history (back to 2010). Leave at 30 for incremental syncs.">
<Input type="number" value={gcForm.sync_lookback_days} min={-1}
onChange={e => setGcForm(f => ({ ...f, sync_lookback_days: parseInt(e.target.value, 10) || 30 }))} />
{gcForm.sync_lookback_days > 365 && gcForm.sync_lookback_days !== -1 && (
<p className="text-yellow-400 text-xs mt-1">Warning: syncing more than 365 days at once may take a long time and could trigger Garmin rate limits.</p>
)}
</Field>
</div> </div>
{gcError && <p className="text-red-400 text-xs">{gcError}</p>} {gcError && <p className="text-red-400 text-xs">{gcError}</p>}