diff --git a/bincio/serve/deps.py b/bincio/serve/deps.py index d0beab3..f648db9 100644 --- a/bincio/serve/deps.py +++ b/bincio/serve/deps.py @@ -43,6 +43,8 @@ sync_secret: str = "" _db = None _strava_sync_running = False _strava_sync_lock = threading.Lock() +_garmin_sync_running = False +_garmin_sync_lock = threading.Lock() # ── Constants ───────────────────────────────────────────────────────────────── diff --git a/bincio/serve/routers/admin.py b/bincio/serve/routers/admin.py index 25687bc..4b294a0 100644 --- a/bincio/serve/routers/admin.py +++ b/bincio/serve/routers/admin.py @@ -585,6 +585,74 @@ async def admin_strava_sync_run( return JSONResponse({"ok": True}, status_code=202) +@router.get("/api/admin/garmin-sync") +async def admin_garmin_sync_status( + bincio_session: str | None = Cookie(default=None), +) -> JSONResponse: + """Return per-user Garmin sync status for the admin panel.""" + deps._require_admin(bincio_session) + root = deps._get_data_dir() + users = [] + for cf in sorted(root.glob("*/garmin_creds.json")): + user_dir = cf.parent + handle = user_dir.name + + run_status: str | None = None + run_imported = 0 + run_errors = 0 + run_error_message: str | None = None + last_run: str | None = None + status_path = user_dir / "_garmin_sync_status.json" + if status_path.exists(): + try: + ss = json.loads(status_path.read_text(encoding="utf-8")) + run_status = ss.get("status") + run_imported = ss.get("imported", 0) + run_errors = ss.get("errors", 0) + run_error_message = ss.get("error_message") + last_run = ss.get("last_run") + except (OSError, json.JSONDecodeError): + pass + + users.append({ + "handle": handle, + "run_status": run_status, + "run_imported": run_imported, + "run_errors": run_errors, + "run_error_message": run_error_message, + "last_run": last_run, + }) + + return JSONResponse({"running": deps._garmin_sync_running, "users": users}) + + +@router.post("/api/admin/garmin-sync/run") +async def admin_garmin_sync_run( + bincio_session: str | None = Cookie(default=None), +) -> JSONResponse: + """Trigger an immediate Garmin sync for all users (admin only).""" + deps._require_admin(bincio_session) + with deps._garmin_sync_lock: + if deps._garmin_sync_running: + raise HTTPException(409, "Sync already running") + deps._garmin_sync_running = True + + def _run() -> None: + try: + from bincio.sync_garmin import sync_all + results = sync_all(deps._get_data_dir()) + total_new = sum(n for n, _ in results.values()) + if total_new > 0: + tasks._site_rebuild_event.set() + except Exception: + log.exception("admin_garmin_sync_run: unexpected error") + finally: + deps._garmin_sync_running = False + + threading.Thread(target=_run, daemon=True, name="admin-garmin-sync").start() + return JSONResponse({"ok": True}, status_code=202) + + @router.post("/api/admin/users/{handle}/recompute-elevation") async def admin_recompute_elevation( handle: str, diff --git a/site/src/pages/admin/index.astro b/site/src/pages/admin/index.astro index 1cd14e7..4d8c45d 100644 --- a/site/src/pages/admin/index.astro +++ b/site/src/pages/admin/index.astro @@ -57,6 +57,29 @@ import Base from '../../layouts/Base.astro';
+| Handle | +Imported (last run) | +Last run | +Status | +
|---|---|---|---|
| Loading… | |||