diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 3862dc19..51fb0c61 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -11,6 +11,7 @@ import { TemperatureChartCanvas } from "@/components/dashboard/scan-terminal/Tem import { TemperatureRunwayDetails } from "@/components/dashboard/scan-terminal/TemperatureRunwayDetails"; import { TemperatureStatsBars } from "@/components/dashboard/scan-terminal/TemperatureStatsBars"; import { rowName } from "@/components/dashboard/scan-terminal/utils"; +import { DASHBOARD_REFRESH_POLICY_MS } from "@/lib/refresh-policy"; import { HOURLY_CACHE_TTL_MS, @@ -58,6 +59,7 @@ const PEAK_GLOW_BADGE_CLASS = { const PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000; const FOREGROUND_FULL_DETAIL_REFRESH_DEDUP_MS = 90_000; +const NO_PATCH_FULL_DETAIL_FALLBACK_MS = DASHBOARD_REFRESH_POLICY_MS.metar; const DETAIL_LOAD_BATCH_DELAY_MS = 0; const INITIAL_DETAIL_LOAD_SLOTS = 3; const DEFERRED_DETAIL_LOAD_DELAY_MS = 1_200; @@ -663,7 +665,7 @@ export function LiveTemperatureThresholdChart({ }; }, [resyncVersion, city, targetResolution]); - // ── SSE fallback: only full-fetch if a visible chart has seen no patch for 2 minutes ── + // ── SSE fallback: only full-fetch if a visible chart has seen no patch for one METAR cadence ── useEffect(() => { if (!shouldPollLiveChart({ city, compact, isActive, isMaximized })) return; let cancelled = false; @@ -687,7 +689,7 @@ export function LiveTemperatureThresholdChart({ const checkFallback = () => { if (typeof document !== "undefined" && document.visibilityState === "hidden") return; - if (Date.now() - lastPatchAtRef.current < 2 * 60_000) return; + if (Date.now() - lastPatchAtRef.current < NO_PATCH_FULL_DETAIL_FALLBACK_MS) return; refreshFullDetail(); }; @@ -966,6 +968,7 @@ export function LiveTemperatureThresholdChart({ ); const subtitle = row ? (isEn ? "Live & Forecast" : "实测与预测") : ""; + const showDetailErrorBadge = !compact || isActive || isMaximized; const handleZoomReset = useCallback(() => { setZoomRange(null); @@ -1259,6 +1262,7 @@ export function LiveTemperatureThresholdChart({ isHourlyLoading={isHourlyLoading} detailError={detailError} showingStaleDetail={showingStaleDetail} + showDetailErrorBadge={showDetailErrorBadge} refAreaLeft={refAreaLeft} refAreaRight={refAreaRight} onMouseDown={handleMouseDown} diff --git a/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx b/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx index b4b49b7d..4759c370 100644 --- a/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx +++ b/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx @@ -112,6 +112,7 @@ function TemperatureChartCanvasComponent({ isHourlyLoading, detailError, showingStaleDetail, + showDetailErrorBadge = true, refAreaLeft, refAreaRight, onMouseDown, @@ -139,6 +140,7 @@ function TemperatureChartCanvasComponent({ isHourlyLoading: boolean; detailError?: string | null; showingStaleDetail?: boolean; + showDetailErrorBadge?: boolean; refAreaLeft: number | null; refAreaRight: number | null; onMouseDown: (event: any) => void; @@ -223,7 +225,7 @@ function TemperatureChartCanvasComponent({ const shouldShowBackgroundRefresh = isHourlyLoading && hasDrawableChartContent; const shouldShowUnavailableState = Boolean(row?.city) && Boolean(detailError) && !isHourlyLoading && !hasDrawableChartContent; const shouldShowBackgroundError = - Boolean(row?.city) && Boolean(detailError) && !isHourlyLoading && hasDrawableChartContent; + showDetailErrorBadge && Boolean(row?.city) && Boolean(detailError) && !isHourlyLoading && hasDrawableChartContent; return (
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts index e883bf8c..cb227912 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts @@ -69,8 +69,9 @@ export async function runTests() { assert( chartSource.includes("useLatestPatch") && chartSource.includes("latestPatch") && - chartSource.includes("2 * 60_000"), - "selected city chart should consume SSE patches and use a 2-minute no-patch fallback", + chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.metar") && + !chartSource.includes("2 * 60_000"), + "selected city chart should consume SSE patches and use a METAR-cadence no-patch fallback instead of a 2-minute forced refresh", ); assert( chartSource.includes("preloadTemperatureChartCanvas"), @@ -184,6 +185,12 @@ export async function runTests() { chartCanvasSourceIncludes(chartSource, "handleRetryDetail"), "city detail charts should show stale cache first and expose a retryable unavailable state", ); + assert( + chartSource.includes("const showDetailErrorBadge = !compact || isActive || isMaximized") && + chartSource.includes("showDetailErrorBadge={showDetailErrorBadge}") && + chartCanvasSourceIncludes(chartSource, "showDetailErrorBadge"), + "compact grid charts should only show the stale-cache retry badge on the active slot or expanded chart", + ); assert( __shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: false, isChartVisible: true }) === true, "visible chart cards should fetch city detail", diff --git a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts index 157049c5..416fde32 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts @@ -148,7 +148,11 @@ export function runTests() { assert(chart.includes("latestPatch"), "temperature chart must react to incoming SSE patches"); assert(chart.includes("useSseResyncVersion"), "temperature chart must resync full detail when SSE replay is incomplete"); assert(chartLogic.includes("runway_points"), "temperature chart must merge v1 runway_points into runway history"); - assert(chart.includes("2 * 60_000"), "temperature chart must wait two minutes without patches before full-fetch fallback"); + assert( + chart.includes("DASHBOARD_REFRESH_POLICY_MS.metar") && + !chart.includes("2 * 60_000"), + "temperature chart must wait one METAR cadence without patches before full-fetch fallback", + ); assert(chart.includes("TemperatureChartCanvas"), "temperature chart shell must compose the extracted chart canvas"); assert(chart.includes("TemperatureStatsBars"), "temperature chart shell must compose the extracted stat bars"); assert(chart.includes("TemperatureRunwayDetails"), "temperature chart shell must compose the extracted runway panel");