feat(frontend): replace inline sync progress with SyncModal
GarminConnection.tsx no longer polls sync status itself or renders any inline "syncing.../last sync..." text -- SyncModal (previous commit) is now the only place sync progress and results are shown. Auth-status polling is simplified to a fixed interval now that it no longer needs to speed up while a sync is running.
This commit is contained in:
@@ -1,49 +1,28 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
import type { AuthResponse, SyncStatus } from "../types/api";
|
import type { AuthResponse } from "../types/api";
|
||||||
|
import { SyncModal } from "./SyncModal";
|
||||||
function syncProgressLabel(status: SyncStatus): string {
|
|
||||||
const { detail_fill_progress: fill, activities_pending_details: pending } = status;
|
|
||||||
if (fill.Total > 0) {
|
|
||||||
// pending includes the current batch, so subtract what's already
|
|
||||||
// counted in this batch's Total to avoid double-counting the "beyond
|
|
||||||
// this batch" remainder.
|
|
||||||
const beyondBatch = Math.max(0, pending - (fill.Total - fill.Done));
|
|
||||||
return `syncing: ${fill.Done}/${fill.Total} activities${beyondBatch > 0 ? ` (+${beyondBatch} more queued)` : ""}`;
|
|
||||||
}
|
|
||||||
return "syncing...";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () => Promise<void> }) {
|
export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () => Promise<void> }) {
|
||||||
const [auth, setAuth] = useState<AuthResponse | null>(null);
|
const [auth, setAuth] = useState<AuthResponse | null>(null);
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
|
||||||
// There's no real Garmin logout -- the backend session just sits idle.
|
// There's no real Garmin logout -- the backend session just sits idle.
|
||||||
// "Disconnect" only hides that and shows Connect again locally; a real
|
// "Disconnect" only hides that and shows Connect again locally; a real
|
||||||
// login (connect()) clears it.
|
// login (connect()) clears it.
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
const [disconnected, setDisconnected] = useState(false);
|
||||||
|
const [showSyncModal, setShowSyncModal] = useState(false);
|
||||||
|
|
||||||
function refreshStatus() {
|
// Auth status alone -- sync status is polled by SyncModal itself while
|
||||||
api.authStatus().then(setAuth).catch((e) => setError(String(e)));
|
// it's open, so this no longer needs the "poll faster while syncing"
|
||||||
api.syncStatus().then(setSyncStatus).catch(() => {});
|
// dynamic interval it used to have.
|
||||||
}
|
|
||||||
|
|
||||||
// Poll faster while a sync is actually running, so progress feels live;
|
|
||||||
// back off to a relaxed interval the rest of the time.
|
|
||||||
const syncStatusRef = useRef(syncStatus);
|
|
||||||
syncStatusRef.current = syncStatus;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshStatus();
|
api.authStatus().then(setAuth).catch((e) => setError(String(e)));
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
const interval = setInterval(() => {
|
||||||
const tick = () => {
|
api.authStatus().then(setAuth).catch(() => {});
|
||||||
refreshStatus();
|
}, 6000);
|
||||||
timeout = setTimeout(tick, syncStatusRef.current?.in_progress ? 1500 : 6000);
|
return () => clearInterval(interval);
|
||||||
};
|
|
||||||
timeout = setTimeout(tick, 1500);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
@@ -83,7 +62,7 @@ export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () =>
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await api.syncRun();
|
await api.syncRun();
|
||||||
refreshStatus();
|
setShowSyncModal(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -174,20 +153,9 @@ export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () =>
|
|||||||
|
|
||||||
{!disconnected && auth?.message && <p className="garmin-connection-message">{auth.message}</p>}
|
{!disconnected && auth?.message && <p className="garmin-connection-message">{auth.message}</p>}
|
||||||
|
|
||||||
{/* Full-width so a long "last sync"/pending-details message has room
|
|
||||||
to breathe, instead of being squeezed next to the status dot. */}
|
|
||||||
{status === "authenticated" && syncStatus?.in_progress && (
|
|
||||||
<p className="garmin-connection-message">{syncProgressLabel(syncStatus)}</p>
|
|
||||||
)}
|
|
||||||
{status === "authenticated" && syncStatus?.last_run && !syncStatus.in_progress && (
|
|
||||||
<p className="garmin-connection-message">
|
|
||||||
last sync: {syncStatus.last_run.Status} ({syncStatus.last_run.ActivitiesFetched} activities)
|
|
||||||
{syncStatus.activities_pending_details > 0 &&
|
|
||||||
` — ${syncStatus.activities_pending_details} still need details, click Sync now again`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
||||||
|
{showSyncModal && <SyncModal onClose={() => setShowSyncModal(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user