Cut Garmin sync API volume; dashboard/health/records/UI improvements
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 9s

Garmin Connect sync:
- Incremental syncs now re-fetch only a 1-day buffer (yesterday + today)
  instead of the full lookback window every run. Full lookback applies on
  the first sync only. Cuts steady-state API calls ~10x.
- Beat interval is now configurable via GARMIN_SYNC_INTERVAL_MINUTES and
  surfaced to the UI; the sync toggle is relabelled to the real cadence.

Frontend:
- Collapsible sidebar; clearer logged-in user + role display.
- Unified Body Battery colouring between dashboard and health (shared util).
- Sleep score trend chart on health page.
- Segments + medals on the dashboard's most-recent activity.
- Segments tab on the Records page.

Repo hygiene: add .gitignore, untrack committed __pycache__/*.pyc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:52:52 +01:00
parent 6a1726e0c3
commit 04689a29bd
22 changed files with 3832 additions and 109 deletions
+28 -23
View File
@@ -17,6 +17,13 @@ from typing import Optional, Tuple
logger = logging.getLogger(__name__)
# On incremental syncs (last_sync_at is set) only re-fetch the last day or two
# rather than the full configured lookback window. A 1-day buffer means the
# window is "yesterday + today", which catches late-arriving / revised data
# (sleep finalised next morning, body battery, manual weight, HRV status, the
# midnight boundary) without re-pulling the same N days on every scheduled run.
INCREMENTAL_BUFFER_DAYS = 1
# ── Password encryption ─────────────────────────────────────────────────────
@@ -78,9 +85,12 @@ def sync_activities(garmin, user_id: int, since: Optional[datetime],
List activities from Garmin Connect, skip any already in the DB, download
FIT ZIPs for new ones, and queue them for processing.
lookback_days controls the start date on every sync:
-1 → full history back to 2010 on first sync, then incremental (since-1d)
N → incremental (since-1d) when since is set; else last N days on first sync
lookback_days only sets the window on the FIRST sync (since is None):
-1 → full history back to 2010
N → last N days
Every subsequent (incremental) sync re-fetches only the last
INCREMENTAL_BUFFER_DAYS days, regardless of lookback_days, to avoid
re-pulling the whole window on every scheduled run.
Returns the number of new activities queued.
"""
import time
@@ -88,15 +98,11 @@ def sync_activities(garmin, user_id: int, since: Optional[datetime],
from app.models.user import Activity
from sqlalchemy import select, func
if lookback_days == -1:
# All-time: full pull on first sync, incremental thereafter
start_date = (since - timedelta(days=1)).date() if since else date(2010, 1, 1)
elif since:
# Use whichever is earlier: one day before last sync OR the configured lookback
# window. This ensures increasing lookback_days actually fetches older data.
incremental = (since - timedelta(days=1)).date()
lookback = date.today() - timedelta(days=max(lookback_days, 1))
start_date = min(incremental, lookback)
if since:
# Incremental: just the recent buffer (cheap, dedup skips already-imported)
start_date = (since - timedelta(days=INCREMENTAL_BUFFER_DAYS)).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()
@@ -195,21 +201,20 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db,
Fetch daily stats / sleep / HRV from the Garmin Connect JSON API for each
day in the window and upsert into health_metrics.
lookback_days controls the window on every sync:
-1 → full history back to 2010 on first sync, then incremental (since-1d)
N → incremental (since-1d) when since is set; else last N days on first sync
lookback_days only sets the window on the FIRST sync (since is None):
-1 → full history back to 2010
N → last N days
Every subsequent (incremental) sync re-fetches only the last
INCREMENTAL_BUFFER_DAYS days so late-finalised data (sleep, body battery,
weight) is corrected without re-pulling the whole window each run.
Returns the number of days upserted.
"""
from sqlalchemy import text
if lookback_days == -1:
start_date = (since - timedelta(days=1)).date() if since else date(2010, 1, 1)
elif since:
# Use whichever is earlier: one day before last sync OR the configured lookback
# window. This ensures increasing lookback_days actually fetches older data.
incremental = (since - timedelta(days=1)).date()
lookback = date.today() - timedelta(days=max(lookback_days, 1))
start_date = min(incremental, lookback)
if since:
start_date = (since - timedelta(days=INCREMENTAL_BUFFER_DAYS)).date()
elif lookback_days == -1:
start_date = date(2010, 1, 1)
else:
start_date = date.today() - timedelta(days=max(lookback_days, 1))
days = (date.today() - start_date).days + 1