Add Strava sync status report and manual trigger to admin panel

Each sync run now writes _strava_sync_status.json per user (status,
imported count, error message). New admin endpoints expose this data
and allow triggering an on-demand sync. The admin page gains a Strava
Sync section showing per-user token/credentials state, total imported,
last sync time, and last-run status with inline error messages.
This commit is contained in:
Davide Scaini
2026-05-08 13:44:23 +02:00
parent 12693dbd60
commit 2287d6e2ee
3 changed files with 240 additions and 3 deletions
+30 -3
View File
@@ -20,13 +20,35 @@ from pathlib import Path
import click
_TOKEN_FILE = "strava_token.json"
_CREDS_FILE = "strava_credentials.json"
_SYNC_FILE = "_strava_sync.json"
_TOKEN_FILE = "strava_token.json"
_CREDS_FILE = "strava_credentials.json"
_SYNC_FILE = "_strava_sync.json"
_STATUS_FILE = "_strava_sync_status.json"
log = logging.getLogger("bincio.sync_strava")
def _write_status(
user_dir: Path,
status: str,
imported: int,
errors: int,
error_message: str | None = None,
) -> None:
payload: dict = {
"status": status,
"imported": imported,
"errors": errors,
"last_run": datetime.now(timezone.utc).isoformat(),
}
if error_message is not None:
payload["error_message"] = error_message
try:
(user_dir / _STATUS_FILE).write_text(json.dumps(payload, indent=2), encoding="utf-8")
except Exception:
pass
def _load_creds(user_dir: Path) -> tuple[str, str] | None:
"""Return (client_id, client_secret) from strava_credentials.json, or None."""
p = user_dir / _CREDS_FILE
@@ -59,6 +81,7 @@ def sync_user(user_dir: Path) -> tuple[int, int]:
creds = _load_creds(user_dir)
if creds is None:
log.debug("sync[%s]: no strava_credentials.json — skipped", handle)
_write_status(user_dir, "no_credentials", 0, 0)
return 0, 0
client_id, client_secret = creds
@@ -67,6 +90,7 @@ def sync_user(user_dir: Path) -> tuple[int, int]:
token = ensure_fresh(user_dir, client_id, client_secret)
except StravaError as exc:
log.error("sync[%s]: token refresh failed: %s", handle, exc)
_write_status(user_dir, "token_error", 0, 1, str(exc))
return 0, 1
access_token = token["access_token"]
@@ -89,6 +113,7 @@ def sync_user(user_dir: Path) -> tuple[int, int]:
all_acts = fetch_activities(access_token, after=after_ts)
except StravaError as exc:
log.error("sync[%s]: fetch_activities failed: %s", handle, exc)
_write_status(user_dir, "api_error", 0, 1, str(exc))
return 0, 1
new_acts = [a for a in all_acts if str(a["id"]) not in imported_ids]
@@ -97,6 +122,7 @@ def sync_user(user_dir: Path) -> tuple[int, int]:
handle, len(new_acts), len(all_acts) - len(new_acts),
)
if not new_acts:
_write_status(user_dir, "ok", 0, 0)
return 0, 0
# Load existing index so we can update it in place
@@ -158,6 +184,7 @@ def sync_user(user_dir: Path) -> tuple[int, int]:
merge_all(user_dir)
log.info("sync[%s]: done — %d imported, %d errors", handle, imported, errors)
_write_status(user_dir, "ok", imported, errors)
return imported, errors