diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ebf3e75..251496a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1314,20 +1314,28 @@ export default function App() { // surface the error — used by the startup retry, since the logbook DB (a remote // MySQL especially) can take a few seconds to connect while the UI is already // mounted, and we don't want to flash "db not available" during that window. + const refreshSeqRef = useRef(0); // guards against an older refresh clobbering a newer one's data const refresh = useCallback(async (silent = false): Promise => { + // Monotonic guard: two refreshes can be in flight at once (e.g. the immediate + // one after a UDP auto-log, which reads the QSO at "R", and the debounced one + // after extsvc:uploaded, which reads it at "Y"). MySQL query latency can make + // the OLDER one resolve LAST and clobber the newer data — the Club Log status + // flicking R→Y→R. Only the most-recently-issued refresh is allowed to apply. + const seq = ++refreshSeqRef.current; try { const f = buildActiveFilter(); const list = await ListQSOFiltered(f as any); const n = await CountQSO(); const hasFilter = !!(f.quick_callsign || (f.conditions && f.conditions.length)); const matched = hasFilter ? await CountQSOFiltered(f as any) : n; + if (seq !== refreshSeqRef.current) return true; // a newer refresh superseded us — drop this stale result setQsos(list); setTotal(n); setMatchCount(matched); setError(''); return true; } catch (e: any) { - if (!silent) setError(String(e?.message ?? e)); + if (!silent && seq === refreshSeqRef.current) setError(String(e?.message ?? e)); return false; } }, [buildActiveFilter]);