Normalize Wunderground labels to METAR

This commit is contained in:
2569718930@qq.com
2026-04-22 23:43:27 +08:00
parent e3a9f7fb00
commit fe804bc915
8 changed files with 81 additions and 34 deletions
+10 -6
View File
@@ -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 &&
@@ -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} 观测口径判断。`;
+13 -10
View File
@@ -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 &&
@@ -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
+5 -1
View File
@@ -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<string>();
return links.filter((link) => {
+5 -1
View File
@@ -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;
+4 -4
View File
@@ -63,9 +63,9 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"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<Locale, Record<string, string>> = {
"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",
+24
View File
@@ -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;
}