From 79552bfae16a477239cee9ac77b07eeb8d2f6728 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 19 Jul 2026 01:47:39 +0200 Subject: [PATCH] fix: Club Log status R->Y->R flicker on UDP auto-log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A UDP auto-logged QSO shows its Club Log (and other) upload status flick R->Y->R even though the upload succeeds. Two refreshes race: the immediate one after the UDP log reads the QSO at "R", the debounced one after extsvc:uploaded reads it at "Y"; variable MySQL latency lets the older (R) result resolve last and clobber the newer (Y) data. Guard refresh() with a monotonic sequence number so only the most recently issued refresh may apply its result — an older in-flight refresh is dropped. Fixes the same class of stale-clobber for every confirmation column. --- frontend/src/App.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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]);