diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index c7a5fc25..27a2e8e5 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -31,6 +31,7 @@ import { normObs, prefersHighFrequencyRunwayResolution, readSessionCache, + selectCompactSecondaryTemp, selectDisplayRunwayTemp, seedHourlyForecastFromRow, shouldPollLiveChart, @@ -635,6 +636,7 @@ export function LiveTemperatureThresholdChart({ }, [chartSeries, userToggledKeys, city, showRunwayDetails]); const { + isHKO, isShenzhen, metarHeaderLabel, metarHighLabel, @@ -642,11 +644,17 @@ export function LiveTemperatureThresholdChart({ runwayHighLabel, } = useMemo(() => getLiveObservationLabels(row, chartHourly), [row, chartHourly]); - const { currentRunwayTemp, observedHighMetar, observedHighRunway } = useMemo( + const { currentMetarTemp, currentRunwayTemp, observedHighMetar, observedHighRunway } = useMemo( () => getObservationDisplayMetrics(row, chartHourly, settlementPlate), [row, chartHourly, settlementPlate], ); const displayRunwayTemp = selectDisplayRunwayTemp(liveTemp, currentRunwayTemp, hasRunwayData); + const displayMetarTemp = selectCompactSecondaryTemp({ + isHKO, + isShenzhen, + displayMetarTemp: currentMetarTemp, + observedHighMetar, + }); const wundergroundDailyHigh = getWundergroundDailyHigh(chartHourly); const localDateStr = chartLocalDate || new Date().toISOString().slice(0, 10); @@ -805,6 +813,7 @@ export function LiveTemperatureThresholdChart({ visible_series: activeSeries.map((item) => item.key), live_temp: liveTemp, current_runway_temp: currentRunwayTemp, + current_metar_temp: currentMetarTemp, observed_high_metar: observedHighMetar, observed_high_runway: observedHighRunway, }); @@ -814,6 +823,7 @@ export function LiveTemperatureThresholdChart({ chartSeries, city, compact, + currentMetarTemp, currentRunwayTemp, detailError, hasRunwayData, @@ -960,6 +970,7 @@ export function LiveTemperatureThresholdChart({ metarHighLabel={metarHighLabel} isShenzhen={isShenzhen} displayRunwayTemp={displayRunwayTemp} + displayMetarTemp={displayMetarTemp} observedHighMetar={observedHighMetar} observedHighRunway={observedHighRunway} wundergroundDailyHigh={wundergroundDailyHigh} @@ -1034,4 +1045,5 @@ export const __getInitialDetailLoadDelayMsForTest = getInitialDetailLoadDelayMs; export const __shouldFetchCityDetailForChartForTest = shouldFetchCityDetailForChart; export const __shouldPollLiveChartForTest = shouldPollLiveChart; export const __mergePatchIntoHourlyForTest = mergePatchIntoHourly; +export const __selectCompactSecondaryTempForTest = selectCompactSecondaryTemp; export const __selectDisplayRunwayTempForTest = selectDisplayRunwayTemp; diff --git a/frontend/components/dashboard/scan-terminal/TemperatureStatsBars.tsx b/frontend/components/dashboard/scan-terminal/TemperatureStatsBars.tsx index 37f3e4f7..4698f3ef 100644 --- a/frontend/components/dashboard/scan-terminal/TemperatureStatsBars.tsx +++ b/frontend/components/dashboard/scan-terminal/TemperatureStatsBars.tsx @@ -76,6 +76,7 @@ export function TemperatureStatsBars({ metarHighLabel, isShenzhen, displayRunwayTemp, + displayMetarTemp, observedHighMetar, observedHighRunway, wundergroundDailyHigh, @@ -97,6 +98,7 @@ export function TemperatureStatsBars({ metarHighLabel: string; isShenzhen: boolean; displayRunwayTemp: number | null; + displayMetarTemp: number | null; observedHighMetar: number | null; observedHighRunway: number | null; wundergroundDailyHigh: number | null; @@ -129,7 +131,7 @@ export function TemperatureStatsBars({ | {labels.compactSecondary}:{" "} - {temp(observedHighMetar, tempSymbol)} + {temp(displayMetarTemp, tempSymbol)} ) : ( diff --git a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts index 0cfbdf88..63271bd2 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts @@ -9,6 +9,7 @@ import { __getVisibleTemperatureSeriesForTest, __isTemperatureSeriesVisibleByDefaultForTest, __mergePatchIntoHourlyForTest, + __selectCompactSecondaryTempForTest, __selectDisplayRunwayTempForTest, } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart"; import { __buildTemperatureTooltipRowsForTest } from "@/components/dashboard/scan-terminal/TemperatureTooltipContent"; @@ -976,6 +977,68 @@ export function runTests() { "runway header should prefer runway-history current temp even when the latest detail payload lacks runway_obs snapshot", ); + const hongKongMetrics = __getObservationDisplayMetricsForTest( + { + city: "hong kong", + local_date: "2026-06-06", + local_time: "16:48", + tz_offset_seconds: 8 * 60 * 60, + current_temp: 30.4, + current_max_so_far: 30.4, + temp_symbol: "°C", + } as any, + { + localTime: "16:48", + times: ["00:00", "12:00", "18:00", "23:00"], + temps: [28.0, 31.0, 29.0, 28.0], + settlementTodayObs: [ + { time: "14:00", temp: 30.4 }, + { time: "16:40", temp: 28.1 }, + ], + airportPrimary: { + temp: 28.7, + obs_time: "2026-06-06T16:48:00+08:00", + source_code: "cowin_obs", + source_label: "CoWIN 6087", + }, + airportPrimaryTodayObs: [ + { time: "16:47", temp: 28.6 }, + { time: "16:48", temp: 28.7 }, + ], + } as any, + null, + ); + assert( + hongKongMetrics.currentRunwayTemp === 28.7, + "Hong Kong primary compact stat should stay on the latest CoWIN reference-station point", + ); + assert( + (hongKongMetrics as any).currentMetarTemp === 28.1, + "Hong Kong secondary compact stat should have the latest HKO point separate from the HKO daily high", + ); + assert( + hongKongMetrics.observedHighMetar === 30.4, + "Hong Kong HKO daily high should still be available for expanded daily-high summaries", + ); + assert( + __selectCompactSecondaryTempForTest({ + isHKO: true, + isShenzhen: false, + displayMetarTemp: (hongKongMetrics as any).currentMetarTemp, + observedHighMetar: hongKongMetrics.observedHighMetar, + }) === 28.1, + "Hong Kong compact HKO stat should render the latest HKO point, not the HKO daily high", + ); + assert( + __selectCompactSecondaryTempForTest({ + isHKO: false, + isShenzhen: false, + displayMetarTemp: 72.0, + observedHighMetar: 73.9, + }) === 73.9, + "non-HKO compact secondary stat should keep the existing daily-high behavior", + ); + const wuhanRunwayChart = __buildTemperatureChartDataForTest( { city: "wuhan", diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index a47dabb9..f8312844 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -845,9 +845,14 @@ function getObservationDisplayMetrics( null; } + const currentMetarTemp = + latestSettlement ?? + latestMetar ?? + airportCurrentTemp ?? + null; const observedHighMetar = airportHigh ?? highSettlement ?? highMetar ?? rowMetarHigh ?? null; - return { currentRunwayTemp, observedHighMetar, observedHighRunway }; + return { currentMetarTemp, currentRunwayTemp, observedHighMetar, observedHighRunway }; } function selectDisplayRunwayTemp( @@ -859,6 +864,23 @@ function selectDisplayRunwayTemp( return liveTemp; } +function selectCompactSecondaryTemp({ + isHKO, + isShenzhen, + displayMetarTemp, + observedHighMetar, +}: { + isHKO: boolean; + isShenzhen: boolean; + displayMetarTemp: number | null; + observedHighMetar: number | null; +}) { + if (isHKO && !isShenzhen && displayMetarTemp !== null) { + return displayMetarTemp; + } + return observedHighMetar; +} + function isSettlementRunway(row: ScanOpportunityRow | null, rwy: string) { const cityKey = normalizeCityKey(row?.city); const settlementPairs = SETTLEMENT_RUNWAY_PAIRS[cityKey] || []; @@ -2485,6 +2507,7 @@ export { normalizeCityKey, prefersHighFrequencyRunwayResolution, readSessionCache, + selectCompactSecondaryTemp, selectDisplayRunwayTemp, seedHourlyForecastFromRow, seriesStats,