Refresh terminal charts on activation

This commit is contained in:
2569718930@qq.com
2026-06-14 08:08:49 +08:00
parent ad1e203adf
commit e2ebf5132d
7 changed files with 123 additions and 11 deletions
+2 -2
View File
@@ -106,8 +106,8 @@ services:
POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000} POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000}
POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true} POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true}
POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: '' POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: ''
POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_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:-2} 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_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_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} POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC:-60}
@@ -617,6 +617,7 @@ function PolyWeatherTerminal({
generatedText, generatedText,
isEn, isEn,
locale, locale,
onTerminalActivated,
onRefresh, onRefresh,
refreshing, refreshing,
rows, rows,
@@ -636,10 +637,12 @@ function PolyWeatherTerminal({
setSelectedRegionKey, setSelectedRegionKey,
visibleRegions, visibleRegions,
toggleRegion, toggleRegion,
terminalActivationRefreshKey,
}: { }: {
generatedText: string; generatedText: string;
isEn: boolean; isEn: boolean;
locale: "zh-CN" | "en-US"; locale: "zh-CN" | "en-US";
onTerminalActivated: () => void;
onRefresh: () => void; onRefresh: () => void;
refreshing: boolean; refreshing: boolean;
rows: ScanOpportunityRow[]; rows: ScanOpportunityRow[];
@@ -659,6 +662,7 @@ function PolyWeatherTerminal({
setSelectedRegionKey: (key: string) => void; setSelectedRegionKey: (key: string) => void;
visibleRegions: Set<string>; visibleRegions: Set<string>;
toggleRegion: (key: string) => void; toggleRegion: (key: string) => void;
terminalActivationRefreshKey: number;
}) { }) {
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -682,6 +686,7 @@ function PolyWeatherTerminal({
return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [searchInputRef, setSearchQuery]); }, [searchInputRef, setSearchQuery]);
const [activeNavKey, setActiveNavKey] = useState<string>("thresholds"); const [activeNavKey, setActiveNavKey] = useState<string>("thresholds");
const previousActiveNavKeyRef = useRef(activeNavKey);
const [onlineCount, setOnlineCount] = useState<number | null>(null); const [onlineCount, setOnlineCount] = useState<number | null>(null);
const [feedbackDraft, setFeedbackDraft] = useState<FeedbackDraft | null>(null); const [feedbackDraft, setFeedbackDraft] = useState<FeedbackDraft | null>(null);
const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0); const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0);
@@ -705,6 +710,13 @@ function PolyWeatherTerminal({
void preloadTemperatureChartCanvas(); void preloadTemperatureChartCanvas();
}, [activeNavKey]); }, [activeNavKey]);
useEffect(() => {
const previousActiveNavKey = previousActiveNavKeyRef.current;
previousActiveNavKeyRef.current = activeNavKey;
if (activeNavKey !== "thresholds" || previousActiveNavKey === "thresholds") return;
onTerminalActivated();
}, [activeNavKey, onTerminalActivated]);
useEffect(() => { useEffect(() => {
const fetchOnline = () => { const fetchOnline = () => {
if (typeof document !== "undefined" && document.visibilityState === "hidden") return; if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
@@ -1078,6 +1090,7 @@ function PolyWeatherTerminal({
row={mobileChartRow} row={mobileChartRow}
allRows={filteredRegionRows} allRows={filteredRegionRows}
compact={false} compact={false}
activationRefreshKey={terminalActivationRefreshKey}
disableClose={true} disableClose={true}
onReportIssue={openChartFeedback} onReportIssue={openChartFeedback}
/> />
@@ -1133,6 +1146,7 @@ function PolyWeatherTerminal({
row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === visibleSlots[maximizedSlotIndex]) || null} row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === visibleSlots[maximizedSlotIndex]) || null}
allRows={filteredRegionRows} allRows={filteredRegionRows}
compact={false} compact={false}
activationRefreshKey={terminalActivationRefreshKey}
onSearchClick={() => setActiveSearchSlotIndex(maximizedSlotIndex)} onSearchClick={() => setActiveSearchSlotIndex(maximizedSlotIndex)}
onMaximize={() => setMaximizedSlotIndex(null)} onMaximize={() => setMaximizedSlotIndex(null)}
onClose={() => { onClose={() => {
@@ -1217,6 +1231,7 @@ function PolyWeatherTerminal({
row={rowForSlot} row={rowForSlot}
allRows={filteredRegionRows} allRows={filteredRegionRows}
compact={true} compact={true}
activationRefreshKey={terminalActivationRefreshKey}
isActive={isSlotActive} isActive={isSlotActive}
slotIndex={slotIndex} slotIndex={slotIndex}
onSearchClick={() => setActiveSearchSlotIndex(slotIndex)} onSearchClick={() => setActiveSearchSlotIndex(slotIndex)}
@@ -1639,11 +1654,16 @@ function ScanTerminalScreen() {
setUseLocalTimezoneDefault(false); setUseLocalTimezoneDefault(false);
setSelectedRegionKey(key); setSelectedRegionKey(key);
}, []); }, []);
const [terminalActivationRefreshKey, setTerminalActivationRefreshKey] = useState(0);
const handleTerminalActivated = useCallback(() => {
setTerminalActivationRefreshKey((value) => value + 1);
}, []);
const { refreshScanTerminalManually, scanLoading, terminalData } = const { refreshScanTerminalManually, scanLoading, terminalData } =
useScanTerminalQuery({ useScanTerminalQuery({
isPro, isPro,
proAccessLoading: !hydrated || (proAccess.loading && !canUseLocalFullAccess), proAccessLoading: !hydrated || (proAccess.loading && !canUseLocalFullAccess),
terminalActivationRefreshKey,
timezoneOffsetSeconds: useLocalTimezoneDefault ? localTimezoneOffsetSeconds : null, timezoneOffsetSeconds: useLocalTimezoneDefault ? localTimezoneOffsetSeconds : null,
tradingRegion: selectedRegionKey, tradingRegion: selectedRegionKey,
}); });
@@ -1800,6 +1820,7 @@ function ScanTerminalScreen() {
generatedText={generatedText || ""} generatedText={generatedText || ""}
isEn={isEn} isEn={isEn}
locale={locale} locale={locale}
onTerminalActivated={handleTerminalActivated}
onRefresh={handleRefresh} onRefresh={handleRefresh}
refreshing={scanLoading} refreshing={scanLoading}
rows={filteredRows} rows={filteredRows}
@@ -1821,6 +1842,7 @@ function ScanTerminalScreen() {
setSelectedRegionKey={selectRegionManually} setSelectedRegionKey={selectRegionManually}
visibleRegions={visibleRegions} visibleRegions={visibleRegions}
toggleRegion={toggleRegion} toggleRegion={toggleRegion}
terminalActivationRefreshKey={terminalActivationRefreshKey}
/> />
); );
} }
@@ -542,6 +542,7 @@ export function LiveTemperatureThresholdChart({
isMaximized = false, isMaximized = false,
disableClose = false, disableClose = false,
isActive = !compact, isActive = !compact,
activationRefreshKey = 0,
slotIndex = 0, slotIndex = 0,
}: { }: {
isEn: boolean; isEn: boolean;
@@ -555,6 +556,7 @@ export function LiveTemperatureThresholdChart({
isMaximized?: boolean; isMaximized?: boolean;
disableClose?: boolean; disableClose?: boolean;
isActive?: boolean; isActive?: boolean;
activationRefreshKey?: number;
slotIndex?: number; slotIndex?: number;
}) { }) {
const [hourly, setHourly] = useState<HourlyForecast>(null); const [hourly, setHourly] = useState<HourlyForecast>(null);
@@ -986,6 +988,56 @@ export function LiveTemperatureThresholdChart({
}; };
}, [city, compact, isActive, isMaximized, targetResolution, markDetailDegraded, markDetailRequest, applySuccessfulHourlyDetail]); }, [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(() => { useEffect(() => {
if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return; if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return;
let cancelled = false; let cancelled = false;
@@ -1004,9 +1056,9 @@ export function LiveTemperatureThresholdChart({
lastForegroundRefreshAtRef.current = now; lastForegroundRefreshAtRef.current = now;
lastPatchAtRef.current = now; lastPatchAtRef.current = now;
markDetailRequest("force_refresh"); markDetailRequest("network");
fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution }) fetchHourlyForecastForCity(city, { bypassLocalCache: true, resolution: targetResolution })
.then((data) => { .then((data) => {
if (cancelled) return; if (cancelled) return;
if (!data) { if (!data) {
@@ -108,6 +108,20 @@ export async function runTests() {
dashboardSource.includes('activeNavKey !== "thresholds"'), dashboardSource.includes('activeNavKey !== "thresholds"'),
"terminal screen should preload the chart chunk once access is confirmed on the chart tab", "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( assert(
chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") && chartSource.includes("fetchHourlyForecastForCity(city, { ignoreCache: true, resolution: targetResolution })") &&
chartSource.includes("setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row))"), chartSource.includes("setHourly((prev) => mergeHourlyWithLiveObservations(dataWithCurrentRow, prev, row))"),
@@ -249,11 +249,12 @@ 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("ignoreCache: true") && foregroundRefreshBlock.includes("bypassLocalCache: true") &&
foregroundRefreshBlock.includes("fetchHourlyForecastForCity") && foregroundRefreshBlock.includes("fetchHourlyForecastForCity") &&
foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") && foregroundRefreshBlock.includes("FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS") &&
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"), !foregroundRefreshBlock.includes("ignoreCache: true") &&
"foreground resume refresh should update full detail in the background without showing the loading overlay or refetching fresh detail", !foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
"foreground resume refresh should revalidate cached detail in the background without showing the loading overlay or forcing external sources",
); );
assert( assert(
!chart.includes("/api/city/${encodeURIComponent(city)}/summary"), !chart.includes("/api/city/${encodeURIComponent(city)}/summary"),
@@ -85,11 +85,13 @@ function applyTerminalPatches(
export function useScanTerminalQuery({ export function useScanTerminalQuery({
isPro, isPro,
proAccessLoading, proAccessLoading,
terminalActivationRefreshKey = 0,
timezoneOffsetSeconds, timezoneOffsetSeconds,
tradingRegion, tradingRegion,
}: { }: {
isPro: boolean; isPro: boolean;
proAccessLoading: boolean; proAccessLoading: boolean;
terminalActivationRefreshKey?: number;
timezoneOffsetSeconds?: number | null; timezoneOffsetSeconds?: number | null;
tradingRegion?: string; tradingRegion?: string;
}) { }) {
@@ -206,6 +208,27 @@ export function useScanTerminalQuery({
}; };
}, [fetchScanTerminal, isPro, proAccessLoading, scanRemote.status]); }, [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 // Preload adjacent regions in idle time for instant tab switches
useEffect(() => { useEffect(() => {
if (typeof window === "undefined" || !tradingRegion || !isPro) return; if (typeof window === "undefined" || !tradingRegion || !isPro) return;
+2 -2
View File
@@ -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: 'false'" in web_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in warmer_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_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-3}" 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_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_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 "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 assert "UVICORN_WORKERS: ${UVICORN_WORKERS:-2}" in web_block