diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index 2a5995b..d00fd00 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -424,15 +424,22 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { const context = useMemo(() => ({ spotStatus }), [spotStatus]); // Spot statuses arrive asynchronously (~after the rows render). The Call/Band/ - // Mode cellStyles depend on them but their cell VALUE doesn't change, so ag-grid - // won't re-render those cells on its own — force a refresh so e.g. a worked call - // turns blue once its status loads. + // Mode cellStyles and the dimmed "represents nothing" class depend on them but + // the cell VALUE doesn't change, so ag-grid won't re-render on its own — force a + // refresh so e.g. a worked call turns blue once its status loads. + // + // THROTTLED: under an RBN firehose spotStatus updates ~20×/second, and firing a + // full refreshCells that often is pure churn on a slow PC. Coalesce the bursts + // into one refresh every 200 ms (still imperceptible) instead of one per update. + const refreshPending = useRef(undefined); useEffect(() => { - // Light refresh so status-dependent styling — the Call/Band/Mode colours AND - // the dimmed "represents nothing" class (cellClassRules above) — re-applies - // once a status lands, WITHOUT redrawing whole rows (which pegged the CPU). - gridRef.current?.api?.refreshCells({ force: true }); + if (refreshPending.current !== undefined) return; // a refresh is already queued + refreshPending.current = window.setTimeout(() => { + refreshPending.current = undefined; + gridRef.current?.api?.refreshCells({ force: true }); + }, 200); }, [spotStatus]); + useEffect(() => () => { if (refreshPending.current !== undefined) window.clearTimeout(refreshPending.current); }, []); // Restore AFTER the profile scope is known — this grid has no key= remount to // save it from reading the wrong (unscoped) cache key at first paint.