diff --git a/client/src/pages/SyncPage.tsx b/client/src/pages/SyncPage.tsx index d5f5b6b..6a256c5 100644 --- a/client/src/pages/SyncPage.tsx +++ b/client/src/pages/SyncPage.tsx @@ -54,6 +54,11 @@ function SyncPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + /* Which action 强制重试 should retry — the two flows share one error banner + but call different endpoints, and retrying the wrong one either does + nothing (sync with no token bound yet) or silently drops the email/ + password the user just typed. */ + const [retryAction, setRetryAction] = useState<'sync' | 'login' | null>(null); /* Whether the last attempt was refused by the recorded cooldown. Gates the 强制重试 button: offering it unconditionally would invite the very thing the cooldown prevents. */ @@ -99,10 +104,14 @@ function SyncPage() { }, []); // --- Garmin login ------------------------------------------------------- - const startLogin = async (e: React.FormEvent) => { - e.preventDefault(); + /* `force` is only ever true when the user clicks 强制重试 after a refusal — + never the default path, because retrying inside Garmin's real cooldown + window is what extends it (see services/garmin.py's sso_cooldown). */ + const startLogin = async (e?: React.FormEvent, force?: boolean) => { + e?.preventDefault(); setError(''); setMessage(''); + setBlocked(false); if (!email) { setError('请输入 Garmin 邮箱'); return; @@ -114,7 +123,7 @@ function SyncPage() { setLoading(true); try { - const sid = await apiClient.startGarminLogin(password, email); + const sid = await apiClient.startGarminLogin(password, email, force); // The password is only ever needed for this one request. setPassword(''); setSession(sid); @@ -123,6 +132,10 @@ function SyncPage() { beginPolling(sid); } catch (err: any) { setError(errorMessage(err, '登录失败')); + if (err?.response?.status === 429 && err?.response?.data?.status === 'rate_limited') { + setBlocked(true); + setRetryAction('login'); + } } finally { setLoading(false); } @@ -246,6 +259,7 @@ function SyncPage() { setError(''); setMessage(''); setBlocked(false); + setRetryAction('sync'); setLoading(true); try { // 0 means "everything" and -1 "since the last sync"; the backend caps @@ -327,7 +341,9 @@ function SyncPage() { {blocked && (