Clarify METAR anchor labeling in weather forecasts
This commit is contained in:
@@ -2949,10 +2949,33 @@
|
|||||||
|
|
||||||
.root :global(.future-v2-meteorology-copy) {
|
.root :global(.future-v2-meteorology-copy) {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-anchor-row) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-anchor-source) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid rgba(34, 211, 238, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(8, 47, 73, 0.36);
|
||||||
|
color: #67e8f9;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-meteorology-copy h3) {
|
.root :global(.future-v2-meteorology-copy h3) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
@@ -2962,6 +2985,14 @@
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-anchor-rule) {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 860px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-meteorology-meta) {
|
.root :global(.future-v2-meteorology-meta) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -2981,6 +3012,73 @@
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-rail) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.35fr);
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-anchor) {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(96, 165, 250, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(2, 6, 23, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-anchor span),
|
||||||
|
.root :global(.future-v2-decision-grid span) {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-anchor strong) {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-anchor small) {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #67e8f9;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-grid) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-grid div) {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(34, 211, 238, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(2, 6, 23, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-grid strong) {
|
||||||
|
display: block;
|
||||||
|
margin-top: 7px;
|
||||||
|
color: #67e8f9;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-meteorology-paths) {
|
.root :global(.future-v2-meteorology-paths) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -4127,6 +4225,14 @@
|
|||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-rail) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-grid) {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-layout) {
|
.root :global(.future-v2-layout) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -4160,6 +4266,15 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-anchor-row) {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-decision-grid) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-meteorology-copy h3) {
|
.root :global(.future-v2-meteorology-copy h3) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1197,6 +1197,47 @@ export function FutureForecastModal() {
|
|||||||
String(intradayMeteorology.peak_window || "").trim() ||
|
String(intradayMeteorology.peak_window || "").trim() ||
|
||||||
paceView?.peakWindowText ||
|
paceView?.peakWindowText ||
|
||||||
"--";
|
"--";
|
||||||
|
const settlementSourceCode = String(
|
||||||
|
detail.current?.settlement_source || "",
|
||||||
|
).trim().toLowerCase();
|
||||||
|
const settlementStationCode = String(
|
||||||
|
detail.current?.station_code || detail.risk?.icao || "",
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
const settlementStationName =
|
||||||
|
String(detail.current?.station_name || detail.risk?.airport || "").trim() ||
|
||||||
|
settlementStationCode ||
|
||||||
|
(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" : "官方观测");
|
||||||
|
const anchorRuleText = airportMetarAnchor
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Airport contract anchor: use the ${anchorSourceLabel} reports. Wunderground is only a history display page when present.`
|
||||||
|
: `机场合约锚点:以 ${anchorSourceLabel} 报文为准;若出现 Wunderground,它只是历史展示页面。`
|
||||||
|
: locale === "en-US"
|
||||||
|
? `Official anchor: use ${anchorSourceLabel} observations for this contract.`
|
||||||
|
: `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`;
|
||||||
|
const nextObservationLabel = airportMetarAnchor
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Next METAR watch"
|
||||||
|
: "下一次 METAR 观察"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Next anchor watch"
|
||||||
|
: "下一次锚点观察";
|
||||||
|
const gapToBaseText =
|
||||||
|
gapToBaseBucket == null
|
||||||
|
? "--"
|
||||||
|
: `${gapToBaseBucket.toFixed(1)}${detail.temp_symbol || "°C"}`;
|
||||||
const syncStatusItems = [
|
const syncStatusItems = [
|
||||||
{
|
{
|
||||||
key: "base",
|
key: "base",
|
||||||
@@ -1334,35 +1375,50 @@ export function FutureForecastModal() {
|
|||||||
{isToday && (
|
{isToday && (
|
||||||
<section className="future-v2-meteorology-brief">
|
<section className="future-v2-meteorology-brief">
|
||||||
<div className="future-v2-meteorology-copy">
|
<div className="future-v2-meteorology-copy">
|
||||||
<div className="modal-section-kicker">
|
<div className="future-v2-anchor-row">
|
||||||
{locale === "en-US" ? "Professional meteorology read" : "专业气象判断"}
|
<div className="modal-section-kicker">
|
||||||
|
{locale === "en-US" ? "Professional meteorology read" : "专业气象判断"}
|
||||||
|
</div>
|
||||||
|
<span className="future-v2-anchor-source">{anchorSourceLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3>{meteorologyHeadline}</h3>
|
<h3>{meteorologyHeadline}</h3>
|
||||||
|
<p className="future-v2-anchor-rule">{anchorRuleText}</p>
|
||||||
<div className="future-v2-meteorology-meta">
|
<div className="future-v2-meteorology-meta">
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Confidence" : "置信度"} ·{" "}
|
{locale === "en-US" ? "Confidence" : "置信度"} ·{" "}
|
||||||
{formatConfidenceLabel(intradayMeteorology.confidence, locale)}
|
{formatConfidenceLabel(intradayMeteorology.confidence, locale)}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Peak window" : "峰值窗口"} · {peakWindowText}
|
{locale === "en-US" ? "Path state" : "路径状态"} · {pathStatus}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Next observation" : "下一观测"} · {nextObservationTime}
|
{nextObservationLabel} · {nextObservationTime}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="future-v2-meteorology-paths">
|
<div className="future-v2-decision-rail">
|
||||||
<div>
|
<div className="future-v2-decision-anchor">
|
||||||
<span>{locale === "en-US" ? "Base case" : "基准路径"}</span>
|
<span>{locale === "en-US" ? "Anchor" : "锚点"}</span>
|
||||||
<strong>{baseCaseBucket || "--"}</strong>
|
<strong>{settlementStationName}</strong>
|
||||||
|
<small>{anchorSourceLabel}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="future-v2-decision-grid">
|
||||||
<span>{locale === "en-US" ? "Upside" : "上修路径"}</span>
|
<div>
|
||||||
<strong>{intradayMeteorology.upside_bucket || "--"}</strong>
|
<span>{locale === "en-US" ? "Base" : "基准"}</span>
|
||||||
</div>
|
<strong>{baseCaseBucket || "--"}</strong>
|
||||||
<div>
|
</div>
|
||||||
<span>{locale === "en-US" ? "Downside" : "下修路径"}</span>
|
<div>
|
||||||
<strong>{intradayMeteorology.downside_bucket || "--"}</strong>
|
<span>{locale === "en-US" ? "Upside" : "上修"}</span>
|
||||||
|
<strong>{intradayMeteorology.upside_bucket || "--"}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{locale === "en-US" ? "Downside" : "下修"}</span>
|
||||||
|
<strong>{intradayMeteorology.downside_bucket || "--"}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{locale === "en-US" ? "Gap to base" : "距基准还差"}</span>
|
||||||
|
<strong>{gapToBaseText}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1653,8 +1709,8 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? "Today's temperature forecast (obs + market)"
|
? "Today's temperature path (anchor obs + models)"
|
||||||
: "今日气温预测(观测 + 市场)"}
|
: "今日气温路径(锚点观测 + 模型)"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<DailyTemperatureChart dateStr={dateStr} />
|
<DailyTemperatureChart dateStr={dateStr} />
|
||||||
@@ -1740,8 +1796,12 @@ export function FutureForecastModal() {
|
|||||||
? confirmationRules
|
? confirmationRules
|
||||||
: [
|
: [
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? "Keep watching the next settlement-source observation."
|
? airportMetarAnchor
|
||||||
: "继续观察下一次结算源报文。",
|
? "Keep watching the next anchor METAR report."
|
||||||
|
: "Keep watching the next official anchor observation."
|
||||||
|
: airportMetarAnchor
|
||||||
|
? "继续观察下一次锚点 METAR 报文。"
|
||||||
|
: "继续观察下一次官方锚点观测。",
|
||||||
]
|
]
|
||||||
).map((rule, index) => (
|
).map((rule, index) => (
|
||||||
<li key={`${rule}-${index}`}>{rule}</li>
|
<li key={`${rule}-${index}`}>{rule}</li>
|
||||||
|
|||||||
@@ -158,7 +158,22 @@ export function HeroSummary() {
|
|||||||
const { weatherIcon, weatherText } = getWeatherSummary(data, locale);
|
const { weatherIcon, weatherText } = getWeatherSummary(data, locale);
|
||||||
const metaItems = getHeroMetaItems(data, locale);
|
const metaItems = getHeroMetaItems(data, locale);
|
||||||
const current = data.current || {};
|
const current = data.current || {};
|
||||||
const settlementSource = String(current.settlement_source || "metar").toUpperCase();
|
const settlementSourceCode = String(current.settlement_source || "metar")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
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();
|
||||||
const isMax =
|
const isMax =
|
||||||
current.max_so_far != null &&
|
current.max_so_far != null &&
|
||||||
current.temp != null &&
|
current.temp != null &&
|
||||||
@@ -198,8 +213,8 @@ export function HeroSummary() {
|
|||||||
<div className="hero-item">
|
<div className="hero-item">
|
||||||
<span className="label">
|
<span className="label">
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? `${settlementSource} Settlement Ref`
|
? `${settlementSource} Anchor`
|
||||||
: `${settlementSource} 结算参考`}
|
: `${settlementSource} 锚点`}
|
||||||
</span>
|
</span>
|
||||||
<span className="value highlight">
|
<span className="value highlight">
|
||||||
{current.wu_settlement != null
|
{current.wu_settlement != null
|
||||||
|
|||||||
@@ -106,7 +106,12 @@ function getObservationSourceTag(detail: CityDetail): string {
|
|||||||
if (code === "hko") return "HKO";
|
if (code === "hko") return "HKO";
|
||||||
if (code === "cwa") return "CWA";
|
if (code === "cwa") return "CWA";
|
||||||
if (code === "noaa") return "NOAA";
|
if (code === "noaa") return "NOAA";
|
||||||
if (code === "wunderground") return "WUNDERGROUND";
|
if (code === "wunderground") {
|
||||||
|
const icao = String(detail.risk?.icao || detail.current?.station_code || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
return icao ? `${icao} METAR` : "METAR";
|
||||||
|
}
|
||||||
if (code === "mgm") return "MGM";
|
if (code === "mgm") return "MGM";
|
||||||
return "METAR";
|
return "METAR";
|
||||||
}
|
}
|
||||||
@@ -819,7 +824,7 @@ export function getTemperatureChartData(
|
|||||||
: observationCode === "wunderground" && usingMetarObservationSource
|
: observationCode === "wunderground" && usingMetarObservationSource
|
||||||
? metarFallbackTag
|
? metarFallbackTag
|
||||||
: observationCode === "wunderground"
|
: observationCode === "wunderground"
|
||||||
? `WU ${String(detail.risk?.icao || "").trim().toUpperCase() || "STATION"}`
|
? metarFallbackTag
|
||||||
: useSettlementObservationSource && shouldUseMetarFallback
|
: useSettlementObservationSource && shouldUseMetarFallback
|
||||||
? metarFallbackTag
|
? metarFallbackTag
|
||||||
: observationCode === "noaa"
|
: observationCode === "noaa"
|
||||||
@@ -3190,8 +3195,7 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
|
|||||||
const isOfficialSource =
|
const isOfficialSource =
|
||||||
sourceCode === "hko" ||
|
sourceCode === "hko" ||
|
||||||
sourceCode === "cwa" ||
|
sourceCode === "cwa" ||
|
||||||
sourceCode === "noaa" ||
|
sourceCode === "noaa";
|
||||||
sourceCode === "wunderground";
|
|
||||||
|
|
||||||
const sourceDisplay = (() => {
|
const sourceDisplay = (() => {
|
||||||
if (sourceCode === "hko") {
|
if (sourceCode === "hko") {
|
||||||
@@ -3211,6 +3215,15 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
|
|||||||
? `${noaaName}${noaaCode ? ` (${noaaCode})` : ""}`
|
? `${noaaName}${noaaCode ? ` (${noaaCode})` : ""}`
|
||||||
: `${noaaName}${noaaCode ? `(${noaaCode})` : ""}`;
|
: `${noaaName}${noaaCode ? `(${noaaCode})` : ""}`;
|
||||||
}
|
}
|
||||||
|
if (sourceCode === "wunderground") {
|
||||||
|
const icao = String(detail.risk?.icao || detail.current?.station_code || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
const stationName = String(
|
||||||
|
detail.current?.station_name || detail.risk?.airport || "",
|
||||||
|
).trim();
|
||||||
|
return `${stationName || icao || "Airport"}${icao ? ` (${icao} METAR)` : " METAR"}`;
|
||||||
|
}
|
||||||
const stationName = String(
|
const stationName = String(
|
||||||
detail.current?.station_name || detail.risk?.airport || "",
|
detail.current?.station_name || detail.risk?.airport || "",
|
||||||
).trim();
|
).trim();
|
||||||
@@ -3343,8 +3356,7 @@ export function getSettlementRiskNarrative(
|
|||||||
const stationTerm =
|
const stationTerm =
|
||||||
sourceCode === "hko" ||
|
sourceCode === "hko" ||
|
||||||
sourceCode === "cwa" ||
|
sourceCode === "cwa" ||
|
||||||
sourceCode === "noaa" ||
|
sourceCode === "noaa"
|
||||||
sourceCode === "wunderground"
|
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? "settlement reference station"
|
? "settlement reference station"
|
||||||
: "结算参考站"
|
: "结算参考站"
|
||||||
|
|||||||
Reference in New Issue
Block a user