diff --git a/frontend/components/dashboard/DetailPanel.tsx b/frontend/components/dashboard/DetailPanel.tsx index 25a2269a..22abbf0b 100644 --- a/frontend/components/dashboard/DetailPanel.tsx +++ b/frontend/components/dashboard/DetailPanel.tsx @@ -17,6 +17,7 @@ import { getRiskBadgeLabel, getTemperatureChartData, } from "@/lib/dashboard-utils"; +import { normalizeObservationSourceLabel } from "@/lib/source-labels"; function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) { const { locale, t } = useI18n(); @@ -178,17 +179,20 @@ export function DetailPanel() { () => getTodayPolymarketUrl(detail, locale), [detail, locale], ); - const basicSettlementLabel = + const basicSettlementLabel = normalizeObservationSourceLabel( selectedSummary?.current?.settlement_source_label || - selectedCityItem?.settlement_source_label || - selectedCityItem?.settlement_source || - (locale === "en-US" ? "Settlement source pending" : "结算口径待确认"); + selectedCityItem?.settlement_source_label || + selectedCityItem?.settlement_source, + locale === "en-US" ? "Settlement source pending" : "结算口径待确认", + ); const basicAirportLabel = selectedCityItem?.airport || selectedSummary?.icao || (locale === "en-US" ? "Airport pending" : "机场待确认"); - const heroSettlementLabel = - detail?.current?.settlement_source_label || basicSettlementLabel; + const heroSettlementLabel = normalizeObservationSourceLabel( + detail?.current?.settlement_source_label, + basicSettlementLabel, + ); const heroAirportLabel = detail?.risk?.airport || basicAirportLabel; const isSparsePanelDetail = Boolean( detail && diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index d453825f..3e6baf91 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -32,6 +32,10 @@ import { getWeatherSummary, } from "@/lib/dashboard-utils"; import { dashboardClient } from "@/lib/dashboard-client"; +import { + normalizeObservationSourceCode, + normalizeObservationSourceLabel, +} from "@/lib/source-labels"; import type { IntradayMeteorologySignal, MarketScan } from "@/lib/dashboard-types"; function normalizeMarketValue(value?: number | null) { @@ -1331,9 +1335,9 @@ export function FutureForecastModal() { String(intradayMeteorology.peak_window || "").trim() || paceView?.peakWindowText || "--"; - const settlementSourceCode = String( + const settlementSourceCode = normalizeObservationSourceCode( detail.current?.settlement_source || "", - ).trim().toLowerCase(); + ); const settlementStationCode = String( detail.current?.station_code || detail.risk?.icao || "", ) @@ -1345,19 +1349,20 @@ export function FutureForecastModal() { (locale === "en-US" ? "Anchor station" : "锚点站"); const airportMetarAnchor = settlementSourceCode === "metar" || - settlementSourceCode === "wunderground" || Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode)); const anchorSourceLabel = airportMetarAnchor ? settlementStationCode ? `${settlementStationCode} METAR` : "METAR" - : detail.current?.settlement_source_label || - detail.current?.settlement_source || - (locale === "en-US" ? "Official observation" : "官方观测"); + : normalizeObservationSourceLabel( + detail.current?.settlement_source_label || + detail.current?.settlement_source, + locale === "en-US" ? "Official observation" : "官方观测", + ); const anchorRuleText = airportMetarAnchor ? locale === "en-US" - ? `Airport contract anchor: use the ${anchorSourceLabel} reports. Wunderground is only a history display page when present.` - : `机场合约锚点:以 ${anchorSourceLabel} 报文为准;若出现 Wunderground,它只是历史展示页面。` + ? `Airport contract anchor: use the ${anchorSourceLabel} reports. Third-party history pages are display-only when present.` + : `机场合约锚点:以 ${anchorSourceLabel} 报文为准;第三方历史页只作为展示入口。` : locale === "en-US" ? `Official anchor: use ${anchorSourceLabel} observations for this contract.` : `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`; diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 371ac306..fc588759 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -20,6 +20,10 @@ import { getTemperatureChartData, getWeatherSummary, } from "@/lib/dashboard-utils"; +import { + normalizeObservationSourceCode, + normalizeObservationSourceLabel, +} from "@/lib/source-labels"; function EmptyState({ text }: { text: string }) { return ( @@ -548,22 +552,21 @@ export function HeroSummary() { const { weatherIcon, weatherText } = getWeatherSummary(data, locale); const metaItems = getHeroMetaItems(data, locale); const current = data.current || {}; - const settlementSourceCode = String(current.settlement_source || "metar") - .trim() - .toLowerCase(); + const settlementSourceCode = normalizeObservationSourceCode( + current.settlement_source || "metar", + ); const settlementIcao = String( current.station_code || data.risk?.icao || "", ) .trim() .toUpperCase(); const settlementSource = - settlementSourceCode === "wunderground" - ? settlementIcao - ? `${settlementIcao} METAR` - : "METAR" - : String(current.settlement_source_label || current.settlement_source || "METAR") - .trim() - .toUpperCase(); + settlementSourceCode === "metar" && settlementIcao + ? `${settlementIcao} METAR` + : normalizeObservationSourceLabel( + current.settlement_source_label || current.settlement_source, + "METAR", + ).toUpperCase(); const isMax = current.max_so_far != null && current.temp != null && diff --git a/frontend/components/dashboard/PolyWeatherDashboard.tsx b/frontend/components/dashboard/PolyWeatherDashboard.tsx index c8b2c77b..6f111272 100644 --- a/frontend/components/dashboard/PolyWeatherDashboard.tsx +++ b/frontend/components/dashboard/PolyWeatherDashboard.tsx @@ -20,6 +20,7 @@ import type { CitySummary, RiskLevel, } from "@/lib/dashboard-types"; +import { normalizeObservationSourceLabel } from "@/lib/source-labels"; const loadHistoryModal = () => import("@/components/dashboard/HistoryModal").then( @@ -214,10 +215,12 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) { detail?.risk?.level; const deviation = summary?.deviation_monitor || detail?.deviation_monitor; const observationSource = - summary?.current?.settlement_source_label || - detail?.current?.settlement_source_label || - city.settlement_source_label || - city.airport; + normalizeObservationSourceLabel( + summary?.current?.settlement_source_label || + detail?.current?.settlement_source_label || + city.settlement_source_label, + "METAR", + ); const probabilityBuckets = detail?.probabilities?.distribution || (detail?.local_date diff --git a/frontend/lib/dashboard-official-sources.ts b/frontend/lib/dashboard-official-sources.ts index af73d989..88b4c935 100644 --- a/frontend/lib/dashboard-official-sources.ts +++ b/frontend/lib/dashboard-official-sources.ts @@ -1,4 +1,5 @@ import type { CityDetail } from "@/lib/dashboard-types"; +import { normalizeObservationSourceLabel } from "@/lib/source-labels"; export type OfficialSourceLink = { label: string; @@ -749,7 +750,10 @@ export function getOfficialSourceLinks(detail: CityDetail): OfficialSourceLink[] }), }; } - return link; + return { + ...link, + label: normalizeObservationSourceLabel(link.label, link.label), + }; }); const seen = new Set(); return links.filter((link) => { diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index f47b4010..8848b95e 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -5,6 +5,7 @@ import { HistoryPoint, NearbyStation, } from "@/lib/dashboard-types"; +import { normalizeObservationSourceLabel } from "@/lib/source-labels"; const METAR_WX_MAP: Record< string, @@ -98,7 +99,10 @@ function getObservationSourceCode(detail: CityDetail): string { } function getObservationSourceTag(detail: CityDetail): string { - const label = String(detail.current?.settlement_source_label || "") + const label = normalizeObservationSourceLabel( + detail.current?.settlement_source_label, + "", + ) .trim() .toUpperCase(); if (label) return label; diff --git a/frontend/lib/i18n.ts b/frontend/lib/i18n.ts index fc0cf413..3e39b581 100644 --- a/frontend/lib/i18n.ts +++ b/frontend/lib/i18n.ts @@ -63,9 +63,9 @@ const MESSAGES: Record> = { "history.loading": "正在获取历史数据...", "history.error": "获取历史信息失败", "history.empty": "近 15 天暂无该城市历史数据", - "history.hitRate": "DEB 结算胜率 (WU)", + "history.hitRate": "DEB 结算胜率 (METAR)", "history.mae": "DEB MAE", - "history.debHitRate": "DEB 结算胜率 (WU)", + "history.debHitRate": "DEB 结算胜率 (METAR)", "history.debMae": "DEB MAE", "history.muMae": "μ MAE", "history.bestModelMae": "最佳单模型 MAE", @@ -230,9 +230,9 @@ const MESSAGES: Record> = { "history.loading": "Loading historical data...", "history.error": "Failed to load historical data", "history.empty": "No historical records for this city in the last 15 days", - "history.hitRate": "DEB Settlement Hit Rate (WU)", + "history.hitRate": "DEB Settlement Hit Rate (METAR)", "history.mae": "DEB MAE", - "history.debHitRate": "DEB Settlement Hit Rate (WU)", + "history.debHitRate": "DEB Settlement Hit Rate (METAR)", "history.debMae": "DEB MAE", "history.muMae": "μ MAE", "history.bestModelMae": "Best Single-model MAE", diff --git a/frontend/lib/source-labels.ts b/frontend/lib/source-labels.ts new file mode 100644 index 00000000..8e39f767 --- /dev/null +++ b/frontend/lib/source-labels.ts @@ -0,0 +1,24 @@ +export function normalizeObservationSourceLabel( + value?: string | null, + fallback = "METAR", +) { + const raw = String(value || "").trim(); + if (!raw) return fallback; + const lowered = raw.toLowerCase(); + if ( + lowered === "wu" || + lowered === "wunderground" + ) { + return "METAR"; + } + if (lowered.includes("wunderground")) { + return raw.replace(/wunderground/gi, "METAR"); + } + return raw; +} + +export function normalizeObservationSourceCode(value?: string | null) { + const code = String(value || "").trim().toLowerCase(); + if (code === "wu" || code === "wunderground") return "metar"; + return code; +}