From e2ebf5132d68dee33b57aade3a7c77c065a1e765 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 14 Jun 2026 08:08:49 +0800 Subject: [PATCH] Refresh terminal charts on activation --- docker-compose.yml | 4 +- .../dashboard/ScanTerminalDashboard.tsx | 22 ++++++++ .../LiveTemperatureThresholdChart.tsx | 56 ++++++++++++++++++- .../__tests__/refreshCadencePolicy.test.ts | 14 +++++ .../__tests__/ssePatchArchitecture.test.ts | 11 ++-- .../scan-terminal/use-scan-terminal-query.ts | 23 ++++++++ tests/test_deployment_runtime_config.py | 4 +- 7 files changed, 123 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8c2c1d72..483fc7c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,8 +106,8 @@ services: POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000} POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true} POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: '' - POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-2} - POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-2} + POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-3} + POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-3} POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000} POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000} POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC:-60} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index d28f4245..70762753 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -617,6 +617,7 @@ function PolyWeatherTerminal({ generatedText, isEn, locale, + onTerminalActivated, onRefresh, refreshing, rows, @@ -636,10 +637,12 @@ function PolyWeatherTerminal({ setSelectedRegionKey, visibleRegions, toggleRegion, + terminalActivationRefreshKey, }: { generatedText: string; isEn: boolean; locale: "zh-CN" | "en-US"; + onTerminalActivated: () => void; onRefresh: () => void; refreshing: boolean; rows: ScanOpportunityRow[]; @@ -659,6 +662,7 @@ function PolyWeatherTerminal({ setSelectedRegionKey: (key: string) => void; visibleRegions: Set; toggleRegion: (key: string) => void; + terminalActivationRefreshKey: number; }) { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -682,6 +686,7 @@ function PolyWeatherTerminal({ return () => window.removeEventListener("keydown", handleKeyDown); }, [searchInputRef, setSearchQuery]); const [activeNavKey, setActiveNavKey] = useState("thresholds"); + const previousActiveNavKeyRef = useRef(activeNavKey); const [onlineCount, setOnlineCount] = useState(null); const [feedbackDraft, setFeedbackDraft] = useState(null); const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0); @@ -705,6 +710,13 @@ function PolyWeatherTerminal({ void preloadTemperatureChartCanvas(); }, [activeNavKey]); + useEffect(() => { + const previousActiveNavKey = previousActiveNavKeyRef.current; + previousActiveNavKeyRef.current = activeNavKey; + if (activeNavKey !== "thresholds" || previousActiveNavKey === "thresholds") return; + onTerminalActivated(); + }, [activeNavKey, onTerminalActivated]); + useEffect(() => { const fetchOnline = () => { if (typeof document !== "undefined" && document.visibilityState === "hidden") return; @@ -1078,6 +1090,7 @@ function PolyWeatherTerminal({ row={mobileChartRow} allRows={filteredRegionRows} compact={false} + activationRefreshKey={terminalActivationRefreshKey} disableClose={true} onReportIssue={openChartFeedback} /> @@ -1133,6 +1146,7 @@ function PolyWeatherTerminal({ row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === visibleSlots[maximizedSlotIndex]) || null} allRows={filteredRegionRows} compact={false} + activationRefreshKey={terminalActivationRefreshKey} onSearchClick={() => setActiveSearchSlotIndex(maximizedSlotIndex)} onMaximize={() => setMaximizedSlotIndex(null)} onClose={() => { @@ -1217,6 +1231,7 @@ function PolyWeatherTerminal({ row={rowForSlot} allRows={filteredRegionRows} compact={true} + activationRefreshKey={terminalActivationRefreshKey} isActive={isSlotActive} slotIndex={slotIndex} onSearchClick={() => setActiveSearchSlotIndex(slotIndex)} @@ -1639,11 +1654,16 @@ function ScanTerminalScreen() { setUseLocalTimezoneDefault(false); setSelectedRegionKey(key); }, []); + const [terminalActivationRefreshKey, setTerminalActivationRefreshKey] = useState(0); + const handleTerminalActivated = useCallback(() => { + setTerminalActivationRefreshKey((value) => value + 1); + }, []); const { refreshScanTerminalManually, scanLoading, terminalData } = useScanTerminalQuery({ isPro, proAccessLoading: !hydrated || (proAccess.loading && !canUseLocalFullAccess), + terminalActivationRefreshKey, timezoneOffsetSeconds: useLocalTimezoneDefault ? localTimezoneOffsetSeconds : null, tradingRegion: selectedRegionKey, }); @@ -1800,6 +1820,7 @@ function ScanTerminalScreen() { generatedText={generatedText || ""} isEn={isEn} locale={locale} + onTerminalActivated={handleTerminalActivated} onRefresh={handleRefresh} refreshing={scanLoading} rows={filteredRows} @@ -1821,6 +1842,7 @@ function ScanTerminalScreen() { setSelectedRegionKey={selectRegionManually} visibleRegions={visibleRegions} toggleRegion={toggleRegion} + terminalActivationRefreshKey={terminalActivationRefreshKey} /> ); } diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 5d93f46f..0a05ff51 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -542,6 +542,7 @@ export function LiveTemperatureThresholdChart({ isMaximized = false, disableClose = false, isActive = !compact, + activationRefreshKey = 0, slotIndex = 0, }: { isEn: boolean; @@ -555,6 +556,7 @@ export function LiveTemperatureThresholdChart({ isMaximized?: boolean; disableClose?: boolean; isActive?: boolean; + activationRefreshKey?: number; slotIndex?: number; }) { const [hourly, setHourly] = useState(null); @@ -986,6 +988,56 @@ export function LiveTemperatureThresholdChart({ }; }, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); + useEffect(() => { + if (!activationRefreshKey) return; + if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return; + if (typeof document !== "undefined" && document.visibilityState !== "visible") return; + + let cancelled = false; + + const refreshActivatedCachedDetail = () => { + const now = Date.now(); + if (now - lastForegroundRefreshAtRef.current < 10_000) return; + + lastForegroundRefreshAtRef.current = now; + lastPatchAtRef.current = now; + markDetailRequest("network"); + + fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) + .then((data) => { + if (cancelled) return; + if (!data) { + markDetailDegraded(); + return; + } + applySuccessfulHourlyDetail(data, { updateLiveTemp: true }); + }) + .catch(() => { + if (!cancelled) { + markDetailDegraded(); + } + }) + .finally(() => { + if (!cancelled) setIsHourlyLoading(false); + }); + }; + + refreshActivatedCachedDetail(); + return () => { + cancelled = true; + }; + }, [ + activationRefreshKey, + city, + compact, + isActive, + isMaximized, + targetResolution, + markDetailDegraded, + markDetailRequest, + applySuccessfulHourlyDetail, + ]); + useEffect(() => { if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return; let cancelled = false; @@ -1004,9 +1056,9 @@ export function LiveTemperatureThresholdChart({ lastForegroundRefreshAtRef.current = now; lastPatchAtRef.current = now; - markDetailRequest("force_refresh"); + markDetailRequest("network"); - fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution }) + fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution }) .then((data) => { if (cancelled) return; if (!data) { diff --git a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts index ca0c2cf1..b8e6529a 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts @@ -108,6 +108,20 @@ export async function runTests() { dashboardSource.includes('activeNavKey !== "thresholds"'), "terminal screen should preload the chart chunk once access is confirmed on the chart tab", ); + assert( + dashboardSource.includes("terminalActivationRefreshKey") && + dashboardSource.includes("setTerminalActivationRefreshKey") && + querySource.includes("terminalActivationRefreshKey") && + querySource.includes("handleTerminalActivationRefresh") && + querySource.includes("fetchScanTerminal({ forceRefresh: false, showLoading: false })"), + "switching back to the terminal tab should trigger a lightweight scan refresh without waiting for browser focus events", + ); + assert( + chartSource.includes("activationRefreshKey") && + chartSource.includes("refreshActivatedCachedDetail") && + chartSource.includes("fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })"), + "switching back to the terminal tab should refresh visible chart detail through cached backend data without forcing external sources", + ); assert( chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") && chartSource.includes("setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row))"), diff --git a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts index 96a022ae..7a7731b9 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts @@ -249,11 +249,12 @@ export function runTests() { ); const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || ""; assert( - foregroundRefreshBlock.includes("ignoreCache: true") && - foregroundRefreshBlock.includes("fetchHourlyForecastForCity") && - foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") && - !foregroundRefreshBlock.includes("setIsHourlyLoading(true)"), - "foreground resume refresh should update full detail in the background without showing the loading overlay or refetching fresh detail", + foregroundRefreshBlock.includes("bypassLocalCache: true") && + foregroundRefreshBlock.includes("fetchHourlyForecastForCity") && + foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") && + !foregroundRefreshBlock.includes("ignoreCache: true") && + !foregroundRefreshBlock.includes("setIsHourlyLoading(true)"), + "foreground resume refresh should revalidate cached detail in the background without showing the loading overlay or forcing external sources", ); assert( !chart.includes("/api/city/${encodeURIComponent(city)}/summary"), diff --git a/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts index 48c40152..5563019b 100644 --- a/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts +++ b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts @@ -85,11 +85,13 @@ function applyTerminalPatches( export function useScanTerminalQuery({ isPro, proAccessLoading, + terminalActivationRefreshKey = 0, timezoneOffsetSeconds, tradingRegion, }: { isPro: boolean; proAccessLoading: boolean; + terminalActivationRefreshKey?: number; timezoneOffsetSeconds?: number | null; tradingRegion?: string; }) { @@ -206,6 +208,27 @@ export function useScanTerminalQuery({ }; }, [fetchScanTerminal, isPro, proAccessLoading, scanRemote.status]); + useEffect(() => { + if (!terminalActivationRefreshKey) return; + if (typeof document !== "undefined" && document.visibilityState !== "visible") return; + if (proAccessLoading || !isPro || scanRemote.status === "loading") return; + + const handleTerminalActivationRefresh = () => { + const now = Date.now(); + if (now - lastForegroundScanRefreshAtRef.current < 10_000) return; + lastForegroundScanRefreshAtRef.current = now; + void fetchScanTerminal({ forceRefresh: false, showLoading: false }); + }; + + handleTerminalActivationRefresh(); + }, [ + fetchScanTerminal, + isPro, + proAccessLoading, + scanRemote.status, + terminalActivationRefreshKey, + ]); + // Preload adjacent regions in idle time for instant tab switches useEffect(() => { if (typeof window === "undefined" || !tradingRegion || !isPro) return; diff --git a/tests/test_deployment_runtime_config.py b/tests/test_deployment_runtime_config.py index 58a6093b..6284078c 100644 --- a/tests/test_deployment_runtime_config.py +++ b/tests/test_deployment_runtime_config.py @@ -110,8 +110,8 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services(): assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in warmer_block - assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-2}" in web_block - assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-2}" in web_block + assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-3}" in web_block + assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-3}" in web_block assert "POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}" in web_block assert "POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}" in web_block assert "UVICORN_WORKERS: ${UVICORN_WORKERS:-2}" in web_block