From b8a19509329efda9548660df3ff93655a7d078d7 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 23 Jun 2026 19:02:31 +0800 Subject: [PATCH] Fix Ankara chart stale MGM observations --- .../__tests__/temperatureChartData.test.ts | 81 +++++++++++++++++++ .../scan-terminal/temperature-chart-logic.ts | 61 ++++++++++++-- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts index 28f7a7d8..f2b7b39b 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts @@ -803,6 +803,87 @@ export function runTests() { "Ankara scan-row-seeded airport-primary curve should default to MGM instead of NOAA MADIS when source metadata is missing", ); + const staleAnkaraDetail = toFullChartDetail({ + localDate: "2026-06-23", + localTime: "13:51", + times: ["12:00", "13:00", "14:00", "15:00"], + temps: [22, 24, 25, 24], + airportPrimary: { + temp: 27.1, + obs_time: "2026-06-23T10:39:00Z", + source_code: "mgm", + source_label: "MGM", + }, + airportPrimaryTodayObs: [["2026-06-23T10:39:00Z", 27.1]], + metarTodayObs: [["2026-06-23T10:20:00Z", 24.0]], + debHourlyPath: { + times: ["12:00", "13:00", "14:00", "15:00"], + temps: [20.5, 21.0, 21.2, 21.4], + }, + modelTimes: ["12:00", "13:00", "14:00", "15:00"], + modelCurves: { ECMWF: [23.8, 24.4, 24.7, 24.0] }, + } as any); + const freshAnkaraObservation = observationPayloadToSnapshot({ + city: "ankara", + name: "ankara", + display_name: "Ankara", + local_date: "2026-06-23", + local_time: "13:30", + utc_offset_seconds: 3 * 60 * 60, + current: { + temp: 24.5, + source_code: "metar", + source_label: "METAR", + settlement_source: "metar", + settlement_source_label: "METAR", + station_code: "LTAC", + obs_time: "2026-06-23T10:30:00Z", + }, + airport_current: { + temp: 24.5, + source_code: "metar", + source_label: "METAR", + station_code: "LTAC", + obs_time: "2026-06-23T10:30:00Z", + }, + airport_primary: { + temp: 24.5, + source_code: "metar", + source_label: "METAR", + station_code: "LTAC", + obs_time: "2026-06-23T10:30:00Z", + }, + timeseries: { + metar_today_obs: [{ time: "13:30", temp: 24.5 }], + }, + metar_today_obs: [{ time: "13:30", temp: 24.5 }], + } as any); + const refreshedAnkaraDetail = mergeObservationSnapshotIntoHourly( + staleAnkaraDetail, + freshAnkaraObservation, + ); + const refreshedAnkaraChart = buildFullDayChartData( + { + city: "ankara", + local_date: "2026-06-23", + local_time: "13:51", + tz_offset_seconds: 3 * 60 * 60, + airport: "LTAC", + temp_symbol: "°C", + } as any, + refreshedAnkaraDetail, + false, + ); + const refreshedAnkaraAirportSeries = refreshedAnkaraChart.series.find((item) => item.key === "madis"); + assert( + !refreshedAnkaraAirportSeries?.values.some((value) => value === 27.1), + "Ankara live observation refresh should discard stale cached MGM airport-primary points when the latest source is METAR", + ); + assert( + refreshedAnkaraAirportSeries?.label === "LTAC METAR", + `Ankara refreshed airport-primary curve should be labelled as LTAC METAR, got ${refreshedAnkaraAirportSeries?.label}`, + ); + const guangzhouRunwayWithBadMadisChart = buildFullDayChartData( { city: "guangzhou", diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index e782f7f3..c0e866c8 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -901,6 +901,16 @@ function canonicalAirportPrimarySourceLabel(hourly: ChartRenderState) { return ""; } +function airportPrimaryHasMetarSource(hourly: ChartRenderState) { + const primary = hourly?.airportPrimary; + const tokens = [ + primary?.source_code, + primary?.source_label, + (primary as any)?.source, + ].map((value) => String(value || "").trim().toLowerCase()); + return tokens.some((value) => value === "metar" || value.includes(" metar")); +} + function airportCodeForSeriesLabel( hourly: ChartRenderState, row?: ScanOpportunityRow | null, @@ -943,12 +953,15 @@ function airportPrimarySeriesLabel( const cityKey = normalizeCityKey(row?.city); const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly); if (canonicalLabel === "MGM") return canonicalLabel; + const stationCode = airportCodeForSeriesLabel(hourly, row); + if (airportPrimaryHasMetarSource(hourly)) { + return stationCode ? `${stationCode} METAR` : "METAR"; + } if ((cityKey === "ankara" || cityKey === "istanbul") && (!canonicalLabel || canonicalLabel === "NOAA MADIS")) { return "MGM"; } const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim(); if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel; - const stationCode = airportCodeForSeriesLabel(hourly, row); const isUsAirport = isUsAirportCode(stationCode); if (!isUsAirport) { if (canonicalLabel && canonicalLabel !== "NOAA MADIS") return canonicalLabel; @@ -1316,6 +1329,17 @@ function conditionObservationTime(source: AirportCurrentConditions | null | unde ); } +function conditionObservationSourceKey(source: AirportCurrentConditions | null | undefined) { + const tokens = [ + source?.source_code, + (source as any)?.source, + source?.source_label, + ].map((value) => String(value || "").trim().toLowerCase()); + if (tokens.some((value) => value === "mgm" || value.includes("turkey_mgm"))) return "mgm"; + if (tokens.some((value) => value === "metar" || value.includes(" metar"))) return "metar"; + return tokens.find(Boolean) || ""; +} + function mergeAirportCondition( base: AirportCurrentConditions | null | undefined, live: AirportCurrentConditions | null | undefined, @@ -1338,6 +1362,19 @@ function mergeAirportCondition( return merged; } +function airportPrimaryObservationSourceChanged( + base: ChartRenderState, + live: ChartRenderState, +) { + const baseSource = + conditionObservationSourceKey(base?.airportPrimary) || + conditionObservationSourceKey(base?.airportCurrent); + const liveSource = + conditionObservationSourceKey(live?.airportPrimary) || + conditionObservationSourceKey(live?.airportCurrent); + return Boolean(baseSource && liveSource && baseSource !== liveSource); +} + function runwayHistoryPointKey(point: Record) { const time = String( point.timestamp ?? @@ -1677,6 +1714,10 @@ function mergeHourlyWithLiveObservations( runway_plate_history: runwayPlateHistory, } as AmosData : detailSource.amos; + const useLiveAirportPrimary = + airportPrimaryObservationSourceChanged(base, live) && + Array.isArray(live.airportPrimaryTodayObs) && + live.airportPrimaryTodayObs.length > 0; return { ...detailSource, localDate, @@ -1696,16 +1737,22 @@ function mergeHourlyWithLiveObservations( : forecastFallback.probabilities || null, runwayPlateHistory, amos, - airportCurrent: mergeAirportCondition(base.airportCurrent, live.airportCurrent, row, localDate), - airportPrimary: mergeAirportCondition(base.airportPrimary, live.airportPrimary, row, localDate), + airportCurrent: useLiveAirportPrimary + ? live.airportCurrent || live.airportPrimary || null + : mergeAirportCondition(base.airportCurrent, live.airportCurrent, row, localDate), + airportPrimary: useLiveAirportPrimary + ? live.airportPrimary || live.airportCurrent || null + : mergeAirportCondition(base.airportPrimary, live.airportPrimary, row, localDate), settlementTodayObs: mergeRawObservationPoints(base.settlementTodayObs, live.settlementTodayObs) as ObsPoint[] | undefined, settlementStationCode: detailSource.settlementStationCode || forecastFallback.settlementStationCode || row?.metar_context?.station || null, settlementStationLabel: detailSource.settlementStationLabel || forecastFallback.settlementStationLabel || null, metarTodayObs: mergeRawObservationPoints(base.metarTodayObs, live.metarTodayObs) as ObsPoint[] | undefined, - airportPrimaryTodayObs: mergeRawObservationPoints( - base.airportPrimaryTodayObs, - live.airportPrimaryTodayObs, - ), + airportPrimaryTodayObs: useLiveAirportPrimary + ? live.airportPrimaryTodayObs + : mergeRawObservationPoints( + base.airportPrimaryTodayObs, + live.airportPrimaryTodayObs, + ), }; }