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("ignoreCache: true") &&
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"), !foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
"foreground resume refresh should update full detail in the background without showing the loading overlay or refetching fresh detail", "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