收敛终端图表详情刷新入口

This commit is contained in:
2569718930@qq.com
2026-06-16 04:46:48 +08:00
parent 1e1a78e946
commit fbea6df175
3 changed files with 176 additions and 159 deletions
@@ -554,6 +554,84 @@ function readCachedHourlyForInitialRow(
return null; return null;
} }
type HourlyDetailFetchOptions = {
ignoreCache?: boolean;
bypassLocalCache?: boolean;
};
type HourlyDetailFetchRequest = {
source: ChartDetailSource;
fetchOptions?: HourlyDetailFetchOptions;
applyOptions?: { updateLiveTemp?: boolean };
showUserError?: boolean;
isCancelled?: () => boolean;
onEmpty?: () => void;
onError?: () => void;
onSettled?: () => void;
};
function useHourlyDetailFetcher({
city,
targetResolution,
markDetailRequest,
markDetailDegraded,
applySuccessfulHourlyDetail,
}: {
city: string;
targetResolution: string;
markDetailRequest: (source: ChartDetailSource) => void;
markDetailDegraded: (options?: { showUserError?: boolean }) => void;
applySuccessfulHourlyDetail: (data: HourlyForecast, options?: { updateLiveTemp?: boolean }) => void;
}) {
return useCallback(
async ({
source,
fetchOptions = {},
applyOptions,
showUserError = false,
isCancelled,
onEmpty,
onError,
onSettled,
}: HourlyDetailFetchRequest) => {
const cancelled = () => Boolean(isCancelled?.());
if (!city || cancelled()) return null;
markDetailRequest(source);
try {
const data = await fetchHourlyForecastForCity(city, {
...fetchOptions,
resolution: targetResolution,
});
if (cancelled()) return null;
if (!data) {
if (onEmpty) {
onEmpty();
} else {
markDetailDegraded({ showUserError });
}
return null;
}
applySuccessfulHourlyDetail(data, applyOptions);
return data;
} catch {
if (!cancelled()) {
if (onError) {
onError();
} else {
markDetailDegraded({ showUserError });
}
}
return null;
} finally {
if (!cancelled()) onSettled?.();
}
},
[city, targetResolution, markDetailRequest, markDetailDegraded, applySuccessfulHourlyDetail],
);
}
// ── Main component ───────────────────────────────────────────────────── // ── Main component ─────────────────────────────────────────────────────
export function LiveTemperatureThresholdChart({ export function LiveTemperatureThresholdChart({
@@ -763,6 +841,14 @@ export function LiveTemperatureThresholdChart({
})); }));
}, [city, targetResolution, getLatestRowSnapshot]); }, [city, targetResolution, getLatestRowSnapshot]);
const runHourlyDetailFetch = useHourlyDetailFetcher({
city,
targetResolution,
markDetailRequest,
markDetailDegraded,
applySuccessfulHourlyDetail,
});
useEffect(() => { useEffect(() => {
if (!city || !currentRowObservationSignature) return; if (!city || !currentRowObservationSignature) return;
if (lastRowObservationSignatureRef.current === currentRowObservationSignature) return; if (lastRowObservationSignatureRef.current === currentRowObservationSignature) return;
@@ -843,7 +929,6 @@ export function LiveTemperatureThresholdChart({
setShowingStaleDetail(false); setShowingStaleDetail(false);
} }
setIsHourlyLoading(true); setIsHourlyLoading(true);
markDetailRequest("network");
let cancelled = false; let cancelled = false;
let retryScheduled = false; let retryScheduled = false;
let retryTimer: ReturnType<typeof setTimeout> | null = null; let retryTimer: ReturnType<typeof setTimeout> | null = null;
@@ -853,45 +938,26 @@ export function LiveTemperatureThresholdChart({
markDetailDegraded(); markDetailDegraded();
retryTimer = setTimeout(() => { retryTimer = setTimeout(() => {
if (cancelled) return; if (cancelled) return;
markDetailRequest("network"); void runHourlyDetailFetch({
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) source: "network",
.then((data) => { fetchOptions: { bypassLocalCache: true },
if (cancelled) return; showUserError: true,
if (!data) { isCancelled: () => cancelled,
markDetailDegraded({ showUserError: true }); onSettled: () => setIsHourlyLoading(false),
return; });
}
applySuccessfulHourlyDetail(data);
})
.catch(() => {
if (!cancelled) {
markDetailDegraded({ showUserError: true });
}
})
.finally(() => {
if (!cancelled) setIsHourlyLoading(false);
});
}, TRANSIENT_DETAIL_RETRY_DELAY_MS); }, TRANSIENT_DETAIL_RETRY_DELAY_MS);
}; };
const timer = setTimeout(() => { const timer = setTimeout(() => {
fetchHourlyForecastForCity(city, { resolution: targetResolution }) void runHourlyDetailFetch({
.then((data) => { source: "network",
if (cancelled) return; showUserError: true,
if (!data) { isCancelled: () => cancelled,
scheduleTransientDetailRetry(); onEmpty: scheduleTransientDetailRetry,
return; onSettled: () => {
} if (!retryScheduled) setIsHourlyLoading(false);
applySuccessfulHourlyDetail(data); },
}) });
.catch(() => {
if (!cancelled) {
markDetailDegraded({ showUserError: true });
}
})
.finally(() => {
if (!cancelled && !retryScheduled) setIsHourlyLoading(false);
});
}, DETAIL_LOAD_BATCH_DELAY_MS); }, DETAIL_LOAD_BATCH_DELAY_MS);
return () => { return () => {
@@ -911,8 +977,7 @@ export function LiveTemperatureThresholdChart({
detailRetryNonce, detailRetryNonce,
getLatestRowSnapshot, getLatestRowSnapshot,
markDetailDegraded, markDetailDegraded,
markDetailRequest, runHourlyDetailFetch,
applySuccessfulHourlyDetail,
]); ]);
useEffect(() => { useEffect(() => {
@@ -950,54 +1015,34 @@ export function LiveTemperatureThresholdChart({
let cancelled = false; let cancelled = false;
const refreshProbabilityOverlayAfterPatch = () => { const refreshProbabilityOverlayAfterPatch = () => {
markDetailRequest("force_refresh"); void runHourlyDetailFetch({
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution }) source: "force_refresh",
.then((data) => { fetchOptions: { ignoreCache: true },
if (cancelled) return; isCancelled: () => cancelled,
if (!data) { });
markDetailDegraded();
return;
}
applySuccessfulHourlyDetail(data);
})
.catch(() => {
if (!cancelled) {
markDetailDegraded();
}
});
}; };
refreshProbabilityOverlayAfterPatch(); refreshProbabilityOverlayAfterPatch();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [latestPatch, city, targetResolution, compact, isActive, isMaximized, getLatestRowSnapshot, runHourlyDetailFetch]);
useEffect(() => { useEffect(() => {
if (!resyncVersion || !city) return; if (!resyncVersion || !city) return;
let cancelled = false; let cancelled = false;
markDetailRequest("force_refresh"); void runHourlyDetailFetch({
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution }) source: "force_refresh",
.then((data) => { fetchOptions: { ignoreCache: true },
if (cancelled) return; isCancelled: () => cancelled,
if (!data) { onSettled: () => {
markDetailDegraded(); setIsHourlyLoading(false);
return; },
} });
applySuccessfulHourlyDetail(data);
})
.catch(() => {
if (!cancelled) {
markDetailDegraded();
}
})
.finally(() => {
if (!cancelled) setIsHourlyLoading(false);
});
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [resyncVersion, city, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [resyncVersion, city, runHourlyDetailFetch]);
// ── SSE fallback: visible charts refresh cached detail at observation cadence if patches stop. ── // ── SSE fallback: visible charts refresh cached detail at observation cadence if patches stop. ──
useEffect(() => { useEffect(() => {
@@ -1007,25 +1052,14 @@ export function LiveTemperatureThresholdChart({
const refreshCachedDetail = () => { const refreshCachedDetail = () => {
const now = Date.now(); const now = Date.now();
lastPatchAtRef.current = now; lastPatchAtRef.current = now;
markDetailRequest("network");
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) void runHourlyDetailFetch({
.then((data) => { source: "network",
if (cancelled) return; fetchOptions: { bypassLocalCache: true },
if (!data) { applyOptions: { updateLiveTemp: true },
markDetailDegraded(); isCancelled: () => cancelled,
return; onSettled: () => setIsHourlyLoading(false),
} });
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
})
.catch(() => {
if (!cancelled) {
markDetailDegraded();
}
})
.finally(() => {
if (!cancelled) setIsHourlyLoading(false);
});
}; };
const checkFallback = () => { const checkFallback = () => {
@@ -1040,7 +1074,7 @@ export function LiveTemperatureThresholdChart({
cancelled = true; cancelled = true;
clearInterval(id); clearInterval(id);
}; };
}, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [city, compact, isActive, isMaximized, targetResolution, runHourlyDetailFetch]);
useEffect(() => { useEffect(() => {
if (!activationRefreshKey) return; if (!activationRefreshKey) return;
@@ -1055,25 +1089,14 @@ export function LiveTemperatureThresholdChart({
lastForegroundRefreshAtRef.current = now; lastForegroundRefreshAtRef.current = now;
lastPatchAtRef.current = now; lastPatchAtRef.current = now;
markDetailRequest("network");
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) void runHourlyDetailFetch({
.then((data) => { source: "network",
if (cancelled) return; fetchOptions: { bypassLocalCache: true },
if (!data) { applyOptions: { updateLiveTemp: true },
markDetailDegraded(); isCancelled: () => cancelled,
return; onSettled: () => setIsHourlyLoading(false),
} });
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
})
.catch(() => {
if (!cancelled) {
markDetailDegraded();
}
})
.finally(() => {
if (!cancelled) setIsHourlyLoading(false);
});
}; };
refreshActivatedCachedDetail(); refreshActivatedCachedDetail();
@@ -1087,9 +1110,7 @@ export function LiveTemperatureThresholdChart({
isActive, isActive,
isMaximized, isMaximized,
targetResolution, targetResolution,
markDetailDegraded, runHourlyDetailFetch,
markDetailRequest,
applySuccessfulHourlyDetail,
]); ]);
useEffect(() => { useEffect(() => {
@@ -1110,22 +1131,13 @@ export function LiveTemperatureThresholdChart({
lastForegroundRefreshAtRef.current = now; lastForegroundRefreshAtRef.current = now;
lastPatchAtRef.current = now; lastPatchAtRef.current = now;
markDetailRequest("network");
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) void runHourlyDetailFetch({
.then((data) => { source: "network",
if (cancelled) return; fetchOptions: { bypassLocalCache: true },
if (!data) { applyOptions: { updateLiveTemp: true },
markDetailDegraded(); isCancelled: () => cancelled,
return; });
}
applySuccessfulHourlyDetail(data, { updateLiveTemp: true });
})
.catch(() => {
if (!cancelled) {
markDetailDegraded();
}
});
}; };
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
@@ -1140,7 +1152,7 @@ export function LiveTemperatureThresholdChart({
document.removeEventListener("visibilitychange", handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
window.removeEventListener("focus", refreshForegroundFullDetail); window.removeEventListener("focus", refreshForegroundFullDetail);
}; };
}, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [city, compact, isActive, isMaximized, targetResolution, runHourlyDetailFetch]);
useEffect(() => { useEffect(() => {
if (!city || !currentCityLocalDate) return; if (!city || !currentCityLocalDate) return;
@@ -1150,27 +1162,20 @@ export function LiveTemperatureThresholdChart({
localDayRolloverFetchDateRef.current = currentCityLocalDate; localDayRolloverFetchDateRef.current = currentCityLocalDate;
let cancelled = false; let cancelled = false;
markDetailRequest("force_refresh"); void runHourlyDetailFetch({
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution }) source: "force_refresh",
.then((data) => { fetchOptions: { ignoreCache: true },
if (cancelled) return; isCancelled: () => cancelled,
if (!data) { onError: () => {
markDetailDegraded(); localDayRolloverFetchDateRef.current = "";
return; markDetailDegraded();
} },
applySuccessfulHourlyDetail(data); });
})
.catch(() => {
if (!cancelled) {
localDayRolloverFetchDateRef.current = "";
markDetailDegraded();
}
});
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [city, currentCityLocalDate, hourly?.localDate, row?.local_date, targetResolution, markDetailDegraded, runHourlyDetailFetch]);
const chartHourly = useMemo<HourlyForecast>(() => { const chartHourly = useMemo<HourlyForecast>(() => {
if (!hourly) return hourly; if (!hourly) return hourly;
@@ -88,11 +88,19 @@ export async function runTests() {
assert( assert(
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") && chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
chartSource.includes("refreshCachedDetail") && chartSource.includes("refreshCachedDetail") &&
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") && chartSource.includes("runHourlyDetailFetch") &&
chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
chartLogicSource.includes("options.bypassLocalCache") && chartLogicSource.includes("options.bypassLocalCache") &&
chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"), chartLogicSource.includes("const forceRefresh = Boolean(options.ignoreCache)"),
"visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources", "visible charts should bypass the five-minute browser detail cache every observation cadence without force-refreshing backend sources",
); );
const componentHourlyFetchCalls = chartSource.match(/fetchHourlyForecastForCity\(city,/g) || [];
assert(
chartSource.includes("function useHourlyDetailFetcher") &&
componentHourlyFetchCalls.length === 1 &&
!/fetchHourlyForecastForCity\(city,[\s\S]*?\)\s*\.then\(/.test(chartSource),
"temperature chart should centralize full-detail fetch lifecycle in useHourlyDetailFetcher instead of duplicating then/catch branches across effects",
);
assert( assert(
chartSource.includes("preloadTemperatureChartCanvas"), chartSource.includes("preloadTemperatureChartCanvas"),
"terminal chart canvas should expose a preload hook for first-paint optimization", "terminal chart canvas should expose a preload hook for first-paint optimization",
@@ -120,11 +128,12 @@ export async function runTests() {
assert( assert(
chartSource.includes("activationRefreshKey") && chartSource.includes("activationRefreshKey") &&
chartSource.includes("refreshActivatedCachedDetail") && chartSource.includes("refreshActivatedCachedDetail") &&
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })"), chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
chartSource.includes("applyOptions: { updateLiveTemp: true }"),
"switching back to the terminal tab should refresh visible chart detail through cached backend data without forcing external sources", "switching back to the terminal tab should refresh visible chart detail through cached backend data without forcing external sources",
); );
assert( assert(
chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") && chartSource.includes("fetchOptions: { ignoreCache: true }") &&
chartSource.includes("const mergedHourly = mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow)") && chartSource.includes("const mergedHourly = mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, latestRow)") &&
chartSource.includes("return mergedHourly;"), chartSource.includes("return mergedHourly;"),
"visible chart fallback must refresh full city detail at the current chart resolution while preserving newer live observations", "visible chart fallback must refresh full city detail at the current chart resolution while preserving newer live observations",
@@ -325,7 +334,7 @@ export async function runTests() {
assert( assert(
chartSource.includes("TRANSIENT_DETAIL_RETRY_DELAY_MS") && chartSource.includes("TRANSIENT_DETAIL_RETRY_DELAY_MS") &&
chartSource.includes("scheduleTransientDetailRetry") && chartSource.includes("scheduleTransientDetailRetry") &&
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") && chartSource.includes("fetchOptions: { bypassLocalCache: true }") &&
chartSource.includes("!retryScheduled"), chartSource.includes("!retryScheduled"),
"cold partial detail-batch misses should stay in loading state and retry cached detail once before showing unavailable", "cold partial detail-batch misses should stay in loading state and retry cached detail once before showing unavailable",
); );
@@ -351,7 +360,7 @@ export async function runTests() {
"live row and SSE patch merges must persist their hourly snapshots so returning to terminal restores runway history immediately", "live row and SSE patch merges must persist their hourly snapshots so returning to terminal restores runway history immediately",
); );
const coldDetailFetchBlock = const coldDetailFetchBlock =
/useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?fetchHourlyForecastForCity\(city, \{ resolution: targetResolution \}\)[\s\S]*?\n \}, \[([\s\S]*?)\]\);/.exec(chartSource)?.[1] || ""; /useEffect\(\(\) => \{\s*if \(!city\) \{[\s\S]*?scheduleTransientDetailRetry[\s\S]*?runHourlyDetailFetch\(\{[\s\S]*?\n \}, \[([\s\S]*?)\]\);/.exec(chartSource)?.[1] || "";
assert( assert(
coldDetailFetchBlock.length > 0 && coldDetailFetchBlock.length > 0 &&
!/^\s*row\s*,?\s*$/m.test(coldDetailFetchBlock), !/^\s*row\s*,?\s*$/m.test(coldDetailFetchBlock),
@@ -362,8 +371,8 @@ export async function runTests() {
.match(/setHourly\(data\);/g) || []; .match(/setHourly\(data\);/g) || [];
assert( assert(
rawSuccessfulSetHourlyCalls.length === 0 && rawSuccessfulSetHourlyCalls.length === 0 &&
(chartSource.match(/applySuccessfulHourlyDetail\(data/g) || []).length >= 5, (chartSource.match(/applySuccessfulHourlyDetail\(data/g) || []).length === 1,
"all successful city detail fetch branches should use the shared success handler", "all successful city detail fetch branches should flow through the shared fetcher and success handler once",
); );
assert( assert(
chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") && chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") &&
@@ -231,14 +231,17 @@ export function runTests() {
); );
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || ""; const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
assert( assert(
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") && fallbackRefreshBlock.includes("runHourlyDetailFetch") &&
fallbackRefreshBlock.includes("fetchOptions: { bypassLocalCache: true }") &&
!fallbackRefreshBlock.includes("ignoreCache: true") && !fallbackRefreshBlock.includes("ignoreCache: true") &&
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"), !fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
"no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay", "no-patch fallback refresh should revalidate through cached backend detail without force-refreshing sources or showing the loading overlay",
); );
const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, targetResolution, applySuccessfulHourlyDetail\]\);/)?.[0] || ""; const resyncBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!resyncVersion \|\| !city\) return;[\s\S]*?\}, \[resyncVersion, city, runHourlyDetailFetch\]\);/)?.[0] || "";
assert( assert(
!resyncBlock.includes("setIsHourlyLoading(true)"), resyncBlock.includes("runHourlyDetailFetch") &&
resyncBlock.includes("fetchOptions: { ignoreCache: true }") &&
!resyncBlock.includes("setIsHourlyLoading(true)"),
"SSE replay resync should refresh full detail in the background without showing the loading overlay", "SSE replay resync should refresh full detail in the background without showing the loading overlay",
); );
assert( assert(
@@ -250,7 +253,7 @@ export function runTests() {
const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || ""; const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
assert( assert(
foregroundRefreshBlock.includes("bypassLocalCache: true") && foregroundRefreshBlock.includes("bypassLocalCache: true") &&
foregroundRefreshBlock.includes("fetchHourlyForecastForCity") && foregroundRefreshBlock.includes("runHourlyDetailFetch") &&
foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") && foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") &&
!foregroundRefreshBlock.includes("ignoreCache: true") && !foregroundRefreshBlock.includes("ignoreCache: true") &&
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"), !foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
@@ -291,11 +294,11 @@ export function runTests() {
"temperature chart must trigger a throttled background probability refresh after live observation patches", "temperature chart must trigger a throttled background probability refresh after live observation patches",
); );
const patchEffectBlock = chart.match( const patchEffectBlock = chart.match(
/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*applySuccessfulHourlyDetail[^\]]*\]\);/, /useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?refreshProbabilityOverlayAfterPatch\(\);[\s\S]*?\}, \[[^\]]*latestPatch[^\]]*runHourlyDetailFetch[^\]]*\]\);/,
)?.[0] || ""; )?.[0] || "";
assert( assert(
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") && patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
patchEffectBlock.includes("ignoreCache: true") && patchEffectBlock.includes("fetchOptions: { ignoreCache: true }") &&
!patchEffectBlock.includes("setIsHourlyLoading(true)"), !patchEffectBlock.includes("setIsHourlyLoading(true)"),
"live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay", "live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay",
); );