fix(cluster): refresh spot statuses by merge, not by clearing the cache
setSpotStatus({}) blanked EVERY spot's status, so logging one contact wiped all
the other NEW/NEW-BAND badges and they popped back a moment later. Instead,
re-fetch the shown spots and OVERWRITE each status in place (merge) — the just-
worked spot updates while the rest stay put, no flicker. Same visibility gate
and 2 s debounce; backend cost is one status batch (one logbook scan) as before.
This commit is contained in:
+46
-13
@@ -1500,34 +1500,67 @@ export default function App() {
|
|||||||
// still need resolving without re-subscribing the cluster:spot listener.
|
// still need resolving without re-subscribing the cluster:spot listener.
|
||||||
const spotStatusRef = useRef(spotStatus);
|
const spotStatusRef = useRef(spotStatus);
|
||||||
useEffect(() => { spotStatusRef.current = spotStatus; }, [spotStatus]);
|
useEffect(() => { spotStatusRef.current = spotStatus; }, [spotStatus]);
|
||||||
// After a QSO is logged, the call/entity/POTA/prefix/slot just worked is no
|
// Mirror of spots so the log-triggered refresh reads the current list without
|
||||||
// longer "new" — but each spot's status is cached and never expires, so the
|
// a stale closure.
|
||||||
// cluster kept showing the old NEW badge until a restart. Dropping the cache
|
const spotsRef = useRef(spots);
|
||||||
// re-evaluates every visible spot against the fresh logbook (the poll refetches
|
useEffect(() => { spotsRef.current = spots; }, [spots]);
|
||||||
// the now-unknown statuses). Kept CHEAP for old machines / big logbooks:
|
// Re-fetch the status of every SHOWN spot and OVERWRITE the cache (merge, never
|
||||||
// • only when the cluster is actually on screen — otherwise it costs nothing;
|
// clear). Overwriting keeps the other NEW badges on screen until their fresh
|
||||||
// • a log made while it's hidden just marks it dirty and refetches ONCE the
|
// value lands, instead of blanking the whole grid and letting the badges pop
|
||||||
// next time you open the cluster;
|
// back a moment later. The backend cost is one status batch regardless of spot
|
||||||
// • debounced, so a fast run coalesces instead of re-scanning per QSO.
|
// count (it scans the logbook once), so this is as cheap as the poll already is.
|
||||||
|
const refreshSpotStatuses = useCallback(async () => {
|
||||||
|
const cur = spotsRef.current;
|
||||||
|
if (!cur.length) return;
|
||||||
|
const queries: { call: string; band: string; mode: string; pota_ref: string }[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const s of cur) {
|
||||||
|
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
|
||||||
|
if (seen.has(k)) continue;
|
||||||
|
seen.add(k);
|
||||||
|
queries.push({ call: s.dx_call, band: s.band ?? '', mode: inferSpotMode(s.comment ?? '', s.freq_hz), pota_ref: (s as any).pota_ref ?? '' });
|
||||||
|
}
|
||||||
|
if (!queries.length) return;
|
||||||
|
try {
|
||||||
|
const res = await ClusterSpotStatuses(queries as any);
|
||||||
|
setSpotStatus((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
for (const r of res) {
|
||||||
|
const k = `${r.call}|${r.band ?? ''}|${(r.mode ?? '').toUpperCase()}`;
|
||||||
|
next[k] = {
|
||||||
|
status: r.status ?? '', country: r.country, continent: (r as any).continent,
|
||||||
|
worked_call: !!(r as any).worked_call, new_county: !!(r as any).new_county,
|
||||||
|
new_pota: !!(r as any).new_pota, new_pfx: !!(r as any).new_pfx, pfx: (r as any).pfx,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch { /* leave the old statuses in place on failure */ }
|
||||||
|
}, []);
|
||||||
|
// After a QSO is logged, refresh the shown spots so a call/entity/POTA/prefix/
|
||||||
|
// slot just worked drops its NEW badge — WITHOUT blanking the others (see the
|
||||||
|
// merge above). Kept cheap: only while the cluster is on screen (a log while
|
||||||
|
// hidden marks it dirty and refreshes ONCE on next open), and debounced so a
|
||||||
|
// fast run coalesces instead of re-scanning per QSO.
|
||||||
const clusterVisibleRef = useRef(false);
|
const clusterVisibleRef = useRef(false);
|
||||||
const spotsDirtyRef = useRef(false);
|
const spotsDirtyRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vis = mainPaneLeft === 'cluster' || mainPaneRight === 'cluster' || activeTab === 'cluster';
|
const vis = mainPaneLeft === 'cluster' || mainPaneRight === 'cluster' || activeTab === 'cluster';
|
||||||
if (vis && !clusterVisibleRef.current && spotsDirtyRef.current) {
|
if (vis && !clusterVisibleRef.current && spotsDirtyRef.current) {
|
||||||
spotsDirtyRef.current = false;
|
spotsDirtyRef.current = false;
|
||||||
setSpotStatus({});
|
void refreshSpotStatuses();
|
||||||
}
|
}
|
||||||
clusterVisibleRef.current = vis;
|
clusterVisibleRef.current = vis;
|
||||||
}, [mainPaneLeft, mainPaneRight, activeTab]);
|
}, [mainPaneLeft, mainPaneRight, activeTab, refreshSpotStatuses]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let t: number | undefined;
|
let t: number | undefined;
|
||||||
const off = EventsOn('qso:logged', () => {
|
const off = EventsOn('qso:logged', () => {
|
||||||
if (!clusterVisibleRef.current) { spotsDirtyRef.current = true; return; }
|
if (!clusterVisibleRef.current) { spotsDirtyRef.current = true; return; }
|
||||||
if (t) window.clearTimeout(t);
|
if (t) window.clearTimeout(t);
|
||||||
t = window.setTimeout(() => setSpotStatus({}), 2000);
|
t = window.setTimeout(() => void refreshSpotStatuses(), 2000);
|
||||||
});
|
});
|
||||||
return () => { off(); if (t) window.clearTimeout(t); };
|
return () => { off(); if (t) window.clearTimeout(t); };
|
||||||
}, []);
|
}, [refreshSpotStatuses]);
|
||||||
// Incoming spots are staged here for a few ms, their status resolved, then
|
// Incoming spots are staged here for a few ms, their status resolved, then
|
||||||
// committed to `spots` together — so a row paints with its NEW BAND/MODE badge
|
// committed to `spots` together — so a row paints with its NEW BAND/MODE badge
|
||||||
// already on, instead of flashing plain text then flipping to the pill.
|
// already on, instead of flashing plain text then flipping to the pill.
|
||||||
|
|||||||
Reference in New Issue
Block a user