- "Last sync: never": The old blocking sync was killed by nginx at 120s before save_token was reached. The activities made it to disk (ingestion happens per-activity as it goes), but the token's

last_sync_at timestamp was never written. After deploying, do a soft reset — it'll set last_sync_at to your most recent activity's timestamp so the next sync only fetches newer ones.
  - Reset 404: Added POST /api/strava/reset to serve/server.py. The soft reset now looks in _merged/index.json first (multi-user path), falling back to index.json.
This commit is contained in:
Davide Scaini
2026-04-10 18:34:53 +02:00
parent 816f103b4c
commit 9fd088c693
+57 -8
View File
@@ -663,16 +663,65 @@ async def strava_status(bincio_session: Optional[str] = Cookie(default=None)) ->
if not strava_client_id: if not strava_client_id:
return JSONResponse({"configured": False, "connected": False, "last_sync": None}) return JSONResponse({"configured": False, "connected": False, "last_sync": None})
dd = _get_data_dir() / user.handle dd = _get_data_dir() / user.handle
token_path = dd / "strava_token.json" from bincio.extract.strava_api import load_token
connected = token_path.exists() token = load_token(dd)
last_sync = None return JSONResponse({
if connected: "configured": True,
"connected": token is not None,
"last_sync": token.get("last_sync_at") if token else None,
})
@app.post("/api/strava/reset")
async def strava_reset(request: Request, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
"""Reset last_sync_at so the next sync re-fetches from a chosen point.
mode=soft — set to the started_at of the most recent activity on disk
(next sync only fetches activities newer than the last known one)
mode=hard — clear last_sync_at entirely
(next sync re-downloads full Strava history, skipping existing files)
"""
user = _require_user(bincio_session)
dd = _get_data_dir() / user.handle
from bincio.extract.strava_api import load_token, save_token
token = load_token(dd)
if token is None:
raise HTTPException(400, "Not connected to Strava")
body = await request.json()
mode = body.get("mode", "soft")
if mode == "hard":
token.pop("last_sync_at", None)
save_token(dd, token)
return JSONResponse({"ok": True, "mode": "hard", "last_sync_at": None})
# soft: find the most recent started_at across the user's merged index
from datetime import datetime, timezone
last_ts: int | None = None
for index_path in [dd / "_merged" / "index.json", dd / "index.json"]:
if not index_path.exists():
continue
try: try:
token = json.loads(token_path.read_text()) index_data = json.loads(index_path.read_text(encoding="utf-8"))
last_sync = token.get("last_sync_at") started_ats = [
a.get("started_at") for a in index_data.get("activities", [])
if a.get("started_at")
]
if started_ats:
latest = max(started_ats)
dt = datetime.fromisoformat(latest.replace("Z", "+00:00"))
last_ts = int(dt.astimezone(timezone.utc).timestamp())
break
except Exception: except Exception:
pass continue
return JSONResponse({"configured": True, "connected": connected, "last_sync": last_sync})
if last_ts is None:
token.pop("last_sync_at", None)
else:
token["last_sync_at"] = last_ts
save_token(dd, token)
return JSONResponse({"ok": True, "mode": "soft", "last_sync_at": last_ts})
@app.get("/api/strava/auth-url") @app.get("/api/strava/auth-url")