fix chart refresh and bot command surface

This commit is contained in:
2569718930@qq.com
2026-06-13 01:34:09 +08:00
parent cf35e55da4
commit f115be1fd3
26 changed files with 328 additions and 532 deletions
@@ -923,7 +923,7 @@ export function LiveTemperatureThresholdChart({
lastPatchAtRef.current = now;
markDetailRequest("network");
fetchHourlyForecastForCity(city, { resolution: targetResolution })
fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
.then((data) => {
if (cancelled) return;
if (!data) {
@@ -20,6 +20,7 @@ import {
__resetHourlyDetailRequestQueueForTest,
__runQueuedHourlyDetailRequestForTest,
clearCityDetailCache,
fetchHourlyForecastForCity,
readCityDetailBatchDiagnostics,
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
@@ -64,6 +65,13 @@ export async function runTests() {
!querySource.includes("window.setInterval"),
"scan list should subscribe to SSE patch state instead of running a 5-minute interval",
);
assert(
querySource.includes("handleForegroundScanRefresh") &&
querySource.includes('document.visibilityState !== "visible"') &&
querySource.includes("SCAN_CACHE_TTL_MS") &&
querySource.includes("fetchScanTerminal({ forceRefresh: false, showLoading: false })"),
"scan list should silently revalidate stale rows when a long-hidden browser tab returns to the foreground",
);
assert(
chartLogicSource.includes("DASHBOARD_REFRESH_POLICY_MS.metar") &&
!chartSource.includes("window.setInterval"),
@@ -79,8 +87,10 @@ export async function runTests() {
assert(
chartSource.includes("NO_PATCH_CACHED_DETAIL_REFRESH_MS = DASHBOARD_REFRESH_POLICY_MS.observation") &&
chartSource.includes("refreshCachedDetail") &&
chartSource.includes("fetchHourlyForecastForCity(city, { resolution: targetResolution })"),
"visible charts should do a lightweight cached detail refresh every observation cadence when no SSE patch arrives",
chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
chartLogicSource.includes("options.bypassLocalCache") &&
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",
);
assert(
chartSource.includes("preloadTemperatureChartCanvas"),
@@ -442,6 +452,7 @@ export async function runTests() {
const originalWindow = (globalThis as any).window;
const originalSessionStorage = (globalThis as any).sessionStorage;
const originalFetch = (globalThis as any).fetch;
const store = new Map<string, string>();
(globalThis as any).window = {};
(globalThis as any).sessionStorage = {
@@ -462,6 +473,50 @@ export async function runTests() {
},
};
try {
clearCityDetailCache();
const revalidationUrls: string[] = [];
let revalidationFetches = 0;
(globalThis as any).fetch = async (url: string) => {
revalidationFetches += 1;
revalidationUrls.push(String(url));
return {
ok: true,
json: async () => ({
cities: ["fallback-revalidate"],
details: {
"fallback-revalidate": {
city: "fallback-revalidate",
hourly: {
times: ["00:00"],
temps: [revalidationFetches],
},
},
},
errors: {},
missing: [],
partial: false,
}),
};
};
const firstDetail = await fetchHourlyForecastForCity("fallback-revalidate", { resolution: "10m" });
const cachedDetail = await fetchHourlyForecastForCity("fallback-revalidate", { resolution: "10m" });
const revalidatedDetail = await fetchHourlyForecastForCity("fallback-revalidate", {
bypassLocalCache: true,
resolution: "10m",
});
assert(
firstDetail?.temps[0] === 1 &&
cachedDetail?.temps[0] === 1 &&
revalidatedDetail?.temps[0] === 2 &&
revalidationFetches === 2,
"bypassLocalCache should revalidate cached chart detail through the network",
);
assert(
revalidationUrls.every((url) => url.includes("force_refresh=false")),
"bypassLocalCache must not force-refresh backend observation sources",
);
clearCityDetailCache();
const cacheKey = "paris:10m";
store.set(
@@ -490,6 +545,7 @@ export async function runTests() {
clearCityDetailCache();
(globalThis as any).window = originalWindow;
(globalThis as any).sessionStorage = originalSessionStorage;
(globalThis as any).fetch = originalFetch;
}
}
@@ -231,10 +231,10 @@ export function runTests() {
);
const fallbackRefreshBlock = chart.match(/const refreshCachedDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
assert(
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { resolution: targetResolution })") &&
fallbackRefreshBlock.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })") &&
!fallbackRefreshBlock.includes("ignoreCache: true") &&
!fallbackRefreshBlock.includes("setIsHourlyLoading(true)"),
"no-patch fallback refresh should update the chart through cached batch detail without force-refreshing 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] || "";
assert(
@@ -1321,6 +1321,7 @@ function mergeRowObservationIntoHourly(
}
type HourlyForecastFetchOptions = {
bypassLocalCache?: boolean;
ignoreCache?: boolean;
resolution?: string;
};
@@ -1561,13 +1562,14 @@ async function fetchHourlyForecastForCity(
const resParam = options.resolution || "10m";
const cacheKey = `${city}:${resParam}`;
const forceRefresh = Boolean(options.ignoreCache);
const bypassLocalCache = forceRefresh || Boolean(options.bypassLocalCache);
if (!forceRefresh) {
if (!bypassLocalCache) {
const cached = readHourlyCacheEntry(cacheKey);
if (cached) {
return cached.data;
}
} else {
} else if (forceRefresh) {
const recentlyRefreshed = readHourlyCacheEntry(cacheKey, {
maxAgeMs: HOURLY_FORCE_REFRESH_DEDUP_MS,
});
@@ -1576,7 +1578,11 @@ async function fetchHourlyForecastForCity(
}
}
const requestKey = options.ignoreCache ? `${city}:${resParam}:live` : `${city}:${resParam}`;
const requestKey = forceRefresh
? `${city}:${resParam}:live`
: bypassLocalCache
? `${city}:${resParam}:revalidate`
: `${city}:${resParam}`;
const pending = _hourlyRequestCache.get(requestKey);
if (pending) return pending;
@@ -103,6 +103,8 @@ export function useScanTerminalQuery({
} = useRemoteDataQuery<ScanTerminalResponse>();
const lastForcedScanRefreshAtRef = useRef(0);
const lastForegroundScanRefreshAtRef = useRef(0);
const lastScanSuccessAtRef = useRef(0);
const patchVersion = useSsePatchVersion();
const [cachedRows, setCachedRows] = useState<ScanTerminalResponse | null>(() => {
if (typeof window !== "undefined") {
@@ -140,7 +142,11 @@ export function useScanTerminalQuery({
tradingRegion,
}),
showLoading,
onSuccess: (data) => { writeScanCache(data, tradingRegion || ""); setCachedRows(data); },
onSuccess: (data) => {
lastScanSuccessAtRef.current = Date.now();
writeScanCache(data, tradingRegion || "");
setCachedRows(data);
},
});
},
[isPro, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion],
@@ -172,6 +178,34 @@ export function useScanTerminalQuery({
void fetchScanTerminal({ forceRefresh: true, showLoading: true });
}, [fetchScanTerminal, terminalData]);
useEffect(() => {
if (typeof window === "undefined" || typeof document === "undefined") return;
if (proAccessLoading || !isPro) return;
const handleForegroundScanRefresh = () => {
if (document.visibilityState !== "visible") return;
if (scanRemote.status === "loading") return;
const now = Date.now();
if (now - lastForegroundScanRefreshAtRef.current < 30_000) return;
if (
lastScanSuccessAtRef.current > 0 &&
now - lastScanSuccessAtRef.current < SCAN_CACHE_TTL_MS
) {
return;
}
lastForegroundScanRefreshAtRef.current = now;
void fetchScanTerminal({ forceRefresh: false, showLoading: false });
};
document.addEventListener("visibilitychange", handleForegroundScanRefresh);
window.addEventListener("focus", handleForegroundScanRefresh);
return () => {
document.removeEventListener("visibilitychange", handleForegroundScanRefresh);
window.removeEventListener("focus", handleForegroundScanRefresh);
};
}, [fetchScanTerminal, isPro, proAccessLoading, scanRemote.status]);
// Preload adjacent regions in idle time for instant tab switches
useEffect(() => {
if (typeof window === "undefined" || !tradingRegion || !isPro) return;