Cut Garmin sync API volume; dashboard/health/records/UI improvements
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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.core.config import settings
|
||||
from app.models.user import User, GarminConnectConfig
|
||||
|
||||
router = APIRouter()
|
||||
@@ -27,6 +28,7 @@ class GarminConfigOut(BaseModel):
|
||||
sync_activities: bool
|
||||
sync_wellness: bool
|
||||
sync_lookback_days: int
|
||||
sync_interval_minutes: int # how often the automatic sync runs
|
||||
last_sync_at: Optional[datetime]
|
||||
last_sync_status: Optional[str]
|
||||
connected: bool
|
||||
@@ -48,6 +50,7 @@ async def get_config(
|
||||
return GarminConfigOut(
|
||||
email="", sync_enabled=False, sync_activities=True,
|
||||
sync_wellness=True, sync_lookback_days=30,
|
||||
sync_interval_minutes=settings.garmin_sync_interval_minutes,
|
||||
last_sync_at=None, last_sync_status=None, connected=False,
|
||||
)
|
||||
return GarminConfigOut(
|
||||
@@ -56,6 +59,7 @@ async def get_config(
|
||||
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,
|
||||
sync_interval_minutes=settings.garmin_sync_interval_minutes,
|
||||
last_sync_at=cfg.last_sync_at,
|
||||
last_sync_status=cfg.last_sync_status,
|
||||
connected=True,
|
||||
@@ -121,6 +125,7 @@ async def save_config(
|
||||
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,
|
||||
sync_interval_minutes=settings.garmin_sync_interval_minutes,
|
||||
last_sync_at=cfg.last_sync_at,
|
||||
last_sync_status=cfg.last_sync_status,
|
||||
connected=True,
|
||||
|
||||
@@ -22,6 +22,8 @@ class Settings(BaseSettings):
|
||||
pocketid_client_id: Optional[str] = Field(None, env="POCKETID_CLIENT_ID")
|
||||
pocketid_client_secret: Optional[str] = Field(None, env="POCKETID_CLIENT_SECRET")
|
||||
pocketid_allowed_group: Optional[str] = Field(None, env="POCKETID_ALLOWED_GROUP")
|
||||
# Garmin Connect — how often the beat scheduler runs the automatic sync
|
||||
garmin_sync_interval_minutes: int = Field(30, env="GARMIN_SYNC_INTERVAL_MINUTES")
|
||||
# Files
|
||||
file_store_path: str = Field("/data/files", env="FILE_STORE_PATH")
|
||||
# Environment
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,8 @@ celery_app.conf.update(
|
||||
beat_schedule={
|
||||
"sync-garmin-connect": {
|
||||
"task": "sync_all_garmin_connect",
|
||||
"schedule": 1800.0, # every 30 minutes
|
||||
# Interval is configurable via GARMIN_SYNC_INTERVAL_MINUTES (default 30 min)
|
||||
"schedule": float(settings.garmin_sync_interval_minutes * 60),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user