diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 23487491..0062070e 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -2949,10 +2949,33 @@ .root :global(.future-v2-meteorology-copy) { display: grid; + gap: 9px; + min-width: 0; +} + +.root :global(.future-v2-anchor-row) { + display: flex; + align-items: center; + justify-content: space-between; gap: 10px; 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) { margin: 0; color: #f8fafc; @@ -2962,6 +2985,14 @@ 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) { display: flex; flex-wrap: wrap; @@ -2981,6 +3012,73 @@ 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) { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -4127,6 +4225,14 @@ 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) { grid-template-columns: 1fr; } @@ -4160,6 +4266,15 @@ 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) { font-size: 16px; } diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index cc0e9165..8cbdf4c0 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -1197,6 +1197,47 @@ export function FutureForecastModal() { String(intradayMeteorology.peak_window || "").trim() || 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 = [ { key: "base", @@ -1334,35 +1375,50 @@ export function FutureForecastModal() { {isToday && (
-
- {locale === "en-US" ? "Professional meteorology read" : "专业气象判断"} +
+
+ {locale === "en-US" ? "Professional meteorology read" : "专业气象判断"} +
+ {anchorSourceLabel}

{meteorologyHeadline}

+

{anchorRuleText}

{locale === "en-US" ? "Confidence" : "置信度"} ·{" "} {formatConfidenceLabel(intradayMeteorology.confidence, locale)} - {locale === "en-US" ? "Peak window" : "峰值窗口"} · {peakWindowText} + {locale === "en-US" ? "Path state" : "路径状态"} · {pathStatus} - {locale === "en-US" ? "Next observation" : "下一观测"} · {nextObservationTime} + {nextObservationLabel} · {nextObservationTime}
-
-
- {locale === "en-US" ? "Base case" : "基准路径"} - {baseCaseBucket || "--"} +
+
+ {locale === "en-US" ? "Anchor" : "锚点"} + {settlementStationName} + {anchorSourceLabel}
-
- {locale === "en-US" ? "Upside" : "上修路径"} - {intradayMeteorology.upside_bucket || "--"} -
-
- {locale === "en-US" ? "Downside" : "下修路径"} - {intradayMeteorology.downside_bucket || "--"} +
+
+ {locale === "en-US" ? "Base" : "基准"} + {baseCaseBucket || "--"} +
+
+ {locale === "en-US" ? "Upside" : "上修"} + {intradayMeteorology.upside_bucket || "--"} +
+
+ {locale === "en-US" ? "Downside" : "下修"} + {intradayMeteorology.downside_bucket || "--"} +
+
+ {locale === "en-US" ? "Gap to base" : "距基准还差"} + {gapToBaseText} +
@@ -1653,8 +1709,8 @@ export function FutureForecastModal() {

{locale === "en-US" - ? "Today's temperature forecast (obs + market)" - : "今日气温预测(观测 + 市场)"} + ? "Today's temperature path (anchor obs + models)" + : "今日气温路径(锚点观测 + 模型)"}

@@ -1740,8 +1796,12 @@ export function FutureForecastModal() { ? confirmationRules : [ 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) => (
  • {rule}
  • diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index d54fcef3..2c70bad3 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -158,7 +158,22 @@ export function HeroSummary() { const { weatherIcon, weatherText } = getWeatherSummary(data, locale); const metaItems = getHeroMetaItems(data, locale); 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 = current.max_so_far != null && current.temp != null && @@ -198,8 +213,8 @@ export function HeroSummary() {
    {locale === "en-US" - ? `${settlementSource} Settlement Ref` - : `${settlementSource} 结算参考`} + ? `${settlementSource} Anchor` + : `${settlementSource} 锚点`} {current.wu_settlement != null diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index c0aa010e..e8b0f45e 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -106,7 +106,12 @@ function getObservationSourceTag(detail: CityDetail): string { if (code === "hko") return "HKO"; if (code === "cwa") return "CWA"; 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"; return "METAR"; } @@ -819,7 +824,7 @@ export function getTemperatureChartData( : observationCode === "wunderground" && usingMetarObservationSource ? metarFallbackTag : observationCode === "wunderground" - ? `WU ${String(detail.risk?.icao || "").trim().toUpperCase() || "STATION"}` + ? metarFallbackTag : useSettlementObservationSource && shouldUseMetarFallback ? metarFallbackTag : observationCode === "noaa" @@ -3190,8 +3195,7 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN" const isOfficialSource = sourceCode === "hko" || sourceCode === "cwa" || - sourceCode === "noaa" || - sourceCode === "wunderground"; + sourceCode === "noaa"; const sourceDisplay = (() => { if (sourceCode === "hko") { @@ -3211,6 +3215,15 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN" ? `${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( detail.current?.station_name || detail.risk?.airport || "", ).trim(); @@ -3343,8 +3356,7 @@ export function getSettlementRiskNarrative( const stationTerm = sourceCode === "hko" || sourceCode === "cwa" || - sourceCode === "noaa" || - sourceCode === "wunderground" + sourceCode === "noaa" ? isEnglish(locale) ? "settlement reference station" : "结算参考站"