From 37d9a321abd4815af8c30e29e1a774654deb674e Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 21:02:25 +0200 Subject: [PATCH] feat: add sync progress and result feedback to offline settings tab --- client/src/components/Settings/OfflineTab.tsx | 43 ++++++++++++++++++- client/src/sync/tripSyncManager.ts | 28 ++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/client/src/components/Settings/OfflineTab.tsx b/client/src/components/Settings/OfflineTab.tsx index 578e8d45..cdd273f1 100644 --- a/client/src/components/Settings/OfflineTab.tsx +++ b/client/src/components/Settings/OfflineTab.tsx @@ -7,6 +7,7 @@ import { Wifi, RefreshCw, Trash2, Database, Settings2, RotateCcw, CheckCircle } import Section from './Section' import { offlineDb, clearAll } from '../../db/offlineDb' import { tripSyncManager } from '../../sync/tripSyncManager' +import type { SyncProgress } from '../../sync/tripSyncManager' import { mutationQueue } from '../../sync/mutationQueue' import { DEFAULT_SW_CONFIG, @@ -30,6 +31,9 @@ export default function OfflineTab(): React.ReactElement { const [rows, setRows] = useState([]) const [pendingCount, setPendingCount] = useState(0) const [syncing, setSyncing] = useState(false) + const [syncProgress, setSyncProgress] = useState<{ current: number; total: number } | null>(null) + const [syncResult, setSyncResult] = useState<{ ok: number; failed: number } | null>(null) + const syncResultTimerRef = useRef | null>(null) const [clearing, setClearing] = useState(false) const [loading, setLoading] = useState(true) @@ -89,6 +93,10 @@ export default function OfflineTab(): React.ReactElement { } }, []) + useEffect(() => { + return () => { if (syncResultTimerRef.current) clearTimeout(syncResultTimerRef.current) } + }, []) + async function handleSaveConfig() { const validated = validateSwConfig(cacheConfig) setCacheConfig(validated) @@ -122,9 +130,26 @@ export default function OfflineTab(): React.ReactElement { async function handleResync() { setSyncing(true) + setSyncProgress(null) + setSyncResult(null) + if (syncResultTimerRef.current) clearTimeout(syncResultTimerRef.current) + + function handleProgress(p: SyncProgress) { + if (p.phase === 'trip') { + setSyncProgress({ current: p.index + 1, total: p.total }) + } else if (p.phase === 'done') { + setSyncProgress(null) + setSyncResult({ ok: p.ok, failed: p.failed }) + syncResultTimerRef.current = setTimeout(() => setSyncResult(null), 5000) + } + } + try { const timeout = new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 120_000)) - const result = await Promise.race([tripSyncManager.syncAll().then(() => 'done' as const), timeout]) + const result = await Promise.race([ + tripSyncManager.syncAll({ onProgress: handleProgress }).then(() => 'done' as const), + timeout, + ]) if (result === 'timeout') { tripSyncManager.interrupt() console.warn('[OfflineTab] sync timed out after 120 s') @@ -173,7 +198,11 @@ export default function OfflineTab(): React.ReactElement { }} > - {syncing ? 'Syncing…' : 'Re-sync now'} + {syncing + ? syncProgress + ? `Syncing ${syncProgress.current}/${syncProgress.total}…` + : 'Syncing…' + : 'Re-sync now'}