diff --git a/src/App.js b/src/App.js index 71ae577..b8b2eaf 100644 --- a/src/App.js +++ b/src/App.js @@ -25,24 +25,39 @@ function App() { const [availableCollapsed, setAvailableCollapsed] = useState(true); const [adminSettings, setAdminSettings] = useState(null); const [adminSettingsLoading, setAdminSettingsLoading] = useState(false); - const [syncProgress, setSyncProgress] = useState({ active: false, percent: 0, message: '', block: false }); + const [syncProgress, setSyncProgress] = useState({ + active: false, + percent: 0, + message: '', + block: false, + etaSeconds: null + }); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []); const startSyncProgress = useCallback((message, percent, block = false) => { - setSyncProgress({ active: true, percent, message, block }); + setSyncProgress({ active: true, percent, message, block, etaSeconds: null }); }, []); - const updateSyncProgress = useCallback((message, percent) => { + const updateSyncProgress = useCallback((message, percent, extra = {}) => { setSyncProgress((prev) => { if (!prev.active) { return prev; } + let nextPercent = prev.percent ?? 0; + if (typeof percent === 'number' && Number.isFinite(percent)) { + const bounded = Math.min(100, Math.max(percent, 0)); + nextPercent = Math.max(bounded, nextPercent); + } return { ...prev, - message: message || prev.message, - percent: Math.min(100, Math.max(percent, prev.percent)) + message: message ?? prev.message, + percent: nextPercent, + etaSeconds: + Object.prototype.hasOwnProperty.call(extra, 'etaSeconds') && extra.etaSeconds !== undefined + ? extra.etaSeconds + : prev.etaSeconds ?? null }; }); }, []); @@ -52,10 +67,10 @@ function App() { if (!prev.active) { return prev; } - return { ...prev, percent: 100 }; + return { ...prev, percent: 100, etaSeconds: null }; }); setTimeout(() => { - setSyncProgress({ active: false, percent: 0, message: '', block: false }); + setSyncProgress({ active: false, percent: 0, message: '', block: false, etaSeconds: null }); }, 400); }, []); @@ -347,6 +362,7 @@ function App() { } try { let jobStarted = false; + const jobStartedAt = Date.now(); const triggerRefresh = async () => { const response = await authorizedFetch('/api/stores/refresh', { method: 'POST', @@ -376,22 +392,31 @@ function App() { const total = job.total || 0; const processed = job.processed || 0; const percent = total > 0 ? Math.min(95, 10 + Math.round((processed / total) * 80)) : undefined; + let etaSeconds = null; + if (total > 0 && processed > 0) { + const elapsedSeconds = Math.max(1, (Date.now() - jobStartedAt) / 1000); + const rate = processed / elapsedSeconds; + if (rate > 0) { + const remaining = Math.max(0, total - processed); + etaSeconds = Math.round(remaining / rate); + } + } const message = job.currentStore ? `Prüfe ${job.currentStore} (${processed}/${total || '?'})` : 'Betriebe werden geprüft...'; - updateSyncProgress(message, percent); + updateSyncProgress(message, percent, { etaSeconds }); } else if (!job) { if (statusData.storesFresh) { - updateSyncProgress('Betriebe aktuell.', 90); + updateSyncProgress('Betriebe aktuell.', 90, { etaSeconds: null }); completed = true; } else if (!jobStarted) { await triggerRefresh(); await delay(500); } else { - updateSyncProgress('Warte auf Rückmeldung...', undefined); + updateSyncProgress('Warte auf Rückmeldung...', undefined, { etaSeconds: null }); } } else if (job.status === 'done') { - updateSyncProgress('Synchronisierung abgeschlossen', 95); + updateSyncProgress('Synchronisierung abgeschlossen', 95, { etaSeconds: null }); completed = true; } else if (job.status === 'error') { throw new Error(job.error || 'Unbekannter Fehler beim Prüfen der Betriebe.'); @@ -1452,12 +1477,27 @@ function AdminAccessMessage() { ); } +function formatEta(seconds) { + if (seconds == null || seconds === Infinity) { + return null; + } + const clamped = Math.max(0, seconds); + const mins = Math.floor(clamped / 60); + const secs = clamped % 60; + if (mins > 0) { + return `${mins}m ${secs.toString().padStart(2, '0')}s`; + } + return `${secs}s`; +} + function StoreSyncOverlay({ state }) { if (!state?.active) { return null; } const percent = Math.round(state.percent || 0); const backgroundColor = state.block ? 'rgba(255,255,255,0.95)' : 'rgba(15,23,42,0.4)'; + const etaLabel = formatEta(state.etaSeconds); + return (
Die Verzögerung schützt vor Rate-Limits während die Betriebe geprüft werden.