"use client"; import clsx from "clsx"; import { CSSProperties, useEffect, useMemo, useState } from "react"; import type { Locale } from "@/lib/i18n"; import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall"; import { getFutureModalView } from "@/lib/dashboard-utils"; import { getModelView, getProbabilityView } from "@/lib/model-utils"; import { getTodayPaceView } from "@/lib/pace-utils"; import { dashboardClient } from "@/lib/dashboard-client"; import { getWeatherSummary } from "@/lib/weather-summary-utils"; import { normalizeObservationSourceCode, normalizeObservationSourceLabel, } from "@/lib/source-labels"; import type { CityDetail, ForecastModalMode, LoadingState, MarketScan, ProAccessState, } from "@/lib/dashboard-types"; import { FutureForecastForwardView } from "./FutureForecastForwardView"; import { FutureForecastTodayDecisionBrief } from "./FutureForecastTodayDecisionBrief"; import { FutureForecastTodayLayout } from "./FutureForecastTodayLayout"; import { FutureForecastModalHeader } from "./FutureForecastModalHeader"; import { FutureRefreshLock, FutureSyncStatusStrip, type FutureSyncStatusItem, } from "./FutureForecastModalStatus"; import { type FuturePaceSignalItem } from "./FutureForecastTodayCards"; import { TODAY_MARKET_SCAN_AUTO_REFRESH_MS, clamp, formatBucketLabel, formatMarketPercent, getTrendMetricVisual, localizedList, localizedText, parseBucketBoundaries, parseClockMinutes, parseLeadingNumber, } from "./FutureForecastModal.utils"; type FutureForecastModalControls = { closeFutureModal: () => void; forecastModalMode: ForecastModalMode | null; futureModalDate: string | null; loadingState: LoadingState; openFutureModal: (dateStr: string, forceRefresh?: boolean) => Promise; openTodayModal: (forceRefresh?: boolean) => Promise; }; export function FutureForecastModalContent({ modal, proAccess, locale, t, detail, dateStr, }: { modal: FutureForecastModalControls; proAccess: ProAccessState; locale: Locale; t: (key: string, params?: Record) => string; detail: CityDetail; dateStr: string; }) { const isPro = proAccess.subscriptionActive; const isProLoading = proAccess.loading; const hasModalContext = true; const [showDeferredTodaySections, setShowDeferredTodaySections] = useState(false); const [freshMarketScan, setFreshMarketScan] = useState( null, ); useEffect(() => { if (!hasModalContext) { setShowDeferredTodaySections(false); return; } setShowDeferredTodaySections(false); if (typeof window === "undefined") { setShowDeferredTodaySections(true); return; } let cancelled = false; let timeoutId: ReturnType | null = null; let idleId: number | null = null; const reveal = () => { if (!cancelled) { setShowDeferredTodaySections(true); } }; if ("requestIdleCallback" in window) { idleId = window.requestIdleCallback(reveal, { timeout: 600 }); } else { timeoutId = setTimeout(reveal, 120); } return () => { cancelled = true; if (idleId != null && "cancelIdleCallback" in window) { window.cancelIdleCallback(idleId); } if (timeoutId != null) { clearTimeout(timeoutId); } }; }, [dateStr, detail, hasModalContext]); const isToday = modal.forecastModalMode === "today" || (modal.forecastModalMode == null && dateStr === detail?.local_date); const detailDepth = detail?.detail_depth || "full"; const isFullDetailReady = detailDepth === "full"; const isStructureSyncing = modal.loadingState.futureDeep || !isFullDetailReady; const isAnyLayerSyncing = isStructureSyncing; const isTodayBlockingRefresh = isToday && isStructureSyncing; const activeMarketScan = freshMarketScan || detail?.market_scan || null; useEffect(() => { setFreshMarketScan(null); if (!hasModalContext || !isToday || !isFullDetailReady || !isPro) return; const cityName = String(detail?.name || detail?.display_name || "").trim(); if (!cityName || !dateStr) return; let cancelled = false; let intervalId: ReturnType | null = null; const refreshMarketScan = () => { dashboardClient .getCityMarketScan(cityName, { force: false, lite: false, marketSlug: detail?.market_scan?.primary_market?.slug || null, targetDate: dateStr, }) .then((payload) => { if (cancelled) return; setFreshMarketScan(payload.market_scan || null); }) .catch(() => { if (!cancelled) { setFreshMarketScan(null); } }); }; refreshMarketScan(); intervalId = setInterval(() => { if ( typeof document !== "undefined" && document.visibilityState === "hidden" ) { return; } refreshMarketScan(); }, TODAY_MARKET_SCAN_AUTO_REFRESH_MS); return () => { cancelled = true; if (intervalId != null) { clearInterval(intervalId); } }; }, [ dateStr, detail?.display_name, detail?.local_date, detail?.market_scan?.primary_market?.slug, detail?.name, detail?.updated_at, hasModalContext, isFullDetailReady, isPro, isToday, ]); const view = getFutureModalView(detail, dateStr, locale); const scorePosition = `${50 + view.front.score / 2}%`; const barStyle = { "--score-position": scorePosition, } as CSSProperties & { "--score-position": string }; const weatherSummary = getWeatherSummary(detail, locale); const paceView = useMemo( () => isToday && showDeferredTodaySections ? getTodayPaceView(detail, locale) : null, [detail, isToday, locale, showDeferredTodaySections], ); const probabilityView = useMemo( () => getProbabilityView(detail, dateStr), [dateStr, detail], ); const modelView = useMemo( () => getModelView(detail, dateStr), [dateStr, detail], ); const probabilityEngineKey = String(probabilityView?.engine || "") .trim() .toLowerCase(); const probabilityCalibrationMode = String( probabilityView?.calibrationMode || "", ) .trim() .toLowerCase(); const hasLgbmProbability = useMemo( () => Object.keys(modelView?.models || {}).some((name) => String(name || "") .toLowerCase() .replace(/[\s_/-]/g, "") .includes("lgbm"), ), [modelView], ); const hasEmosProbability = probabilityEngineKey === "emos" || probabilityCalibrationMode.includes("emos"); const probabilityEngineLabel = hasLgbmProbability ? locale === "en-US" ? "LGBM" : "LGBM" : hasEmosProbability ? "EMOS" : locale === "en-US" ? "Calibrated model" : "校准模型"; const probabilityTitle = hasLgbmProbability ? locale === "en-US" ? "LGBM-Calibrated Probability" : "LGBM 校准概率" : hasEmosProbability ? locale === "en-US" ? "EMOS-Calibrated Probability" : "EMOS 校准概率" : locale === "en-US" ? "Calibrated Model Probability" : "校准模型概率"; const topProbabilityBucket = useMemo(() => { const buckets = Array.isArray(probabilityView?.probabilities) ? probabilityView.probabilities : []; return [...buckets] .filter((bucket) => Number.isFinite(Number(bucket?.probability))) .sort((a, b) => Number(b?.probability) - Number(a?.probability))[0]; }, [probabilityView]); const modelSpreadView = useMemo(() => { const values = Object.values(modelView?.models || {}) .map((value) => Number(value)) .filter((value) => Number.isFinite(value)); if (!values.length) return null; const min = Math.min(...values); const max = Math.max(...values); const spread = max - min; return { count: values.length, max, min, spread, }; }, [modelView]); const boundaryRiskView = useMemo(() => { if (!showDeferredTodaySections) return null; if (!isToday || !paceView) return null; const selectedBucket = topProbabilityBucket || null; const bounds = parseBucketBoundaries(selectedBucket); if (!bounds) return null; const projected = paceView.paceAdjustedHigh ?? (detail.deb?.prediction != null ? Number(detail.deb.prediction) : null); if (projected == null || !Number.isFinite(projected)) return null; const distances = [bounds.lower, bounds.upper] .filter( (value): value is number => value != null && Number.isFinite(value), ) .map((value) => ({ boundary: value, gap: Math.abs(projected - value), })) .sort((a, b) => a.gap - b.gap); if (!distances.length) return null; const nearest = distances[0]; const tone = nearest.gap <= 0.4 ? "amber" : nearest.gap <= 0.8 ? "blue" : "cyan"; const status = nearest.gap <= 0.4 ? locale === "en-US" ? "High boundary risk" : "边界风险高" : nearest.gap <= 0.8 ? locale === "en-US" ? "Watch boundary" : "边界需观察" : locale === "en-US" ? "Boundary buffer" : "边界缓冲"; const note = locale === "en-US" ? `${projected.toFixed(1)}${detail.temp_symbol} is ${nearest.gap.toFixed(1)}${detail.temp_symbol} from the nearest boundary ${nearest.boundary.toFixed(1)}°C.` : `${projected.toFixed(1)}${detail.temp_symbol} 距最近边界 ${nearest.boundary.toFixed(1)}°C 还有 ${nearest.gap.toFixed(1)}${detail.temp_symbol}。`; return { label: locale === "en-US" ? "Boundary risk" : "边界风险", note, status, tone, value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`, }; }, [ detail.deb?.prediction, detail.temp_symbol, isToday, locale, paceView, showDeferredTodaySections, topProbabilityBucket, ]); const peakWindowStateView = useMemo(() => { if (!showDeferredTodaySections) return null; if (!isToday || !paceView) return null; const firstHour = Number(detail.peak?.first_h); const lastHour = Number(detail.peak?.last_h); if ( !Number.isFinite(firstHour) || !Number.isFinite(lastHour) || firstHour < 0 || lastHour < firstHour ) { return null; } const currentMinutes = parseClockMinutes(detail.local_time); const startMinutes = firstHour * 60; const endMinutes = (lastHour + 1) * 60; let status = locale === "en-US" ? "Awaiting peak" : "未进入峰值"; let tone: "amber" | "blue" | "cyan" = "blue"; if (currentMinutes != null && currentMinutes >= endMinutes) { status = locale === "en-US" ? "Past peak" : "已过峰值"; tone = "cyan"; } else if (currentMinutes != null && currentMinutes >= startMinutes) { status = locale === "en-US" ? "Peak window live" : "峰值窗口进行中"; tone = "amber"; } const note = locale === "en-US" ? `Primary peak window ${paceView.peakWindowText}.` : `核心峰值窗口 ${paceView.peakWindowText}。`; return { label: locale === "en-US" ? "Peak window" : "峰值窗口状态", note, status, tone, value: paceView.peakWindowText, }; }, [ detail.local_time, detail.peak?.first_h, detail.peak?.last_h, isToday, locale, paceView, showDeferredTodaySections, ]); const networkLeadView = useMemo(() => { if (!showDeferredTodaySections) return null; if (!isToday) return null; const delta = Number(detail.airport_vs_network_delta); const leadSignal = detail.network_lead_signal; if (!Number.isFinite(delta)) return null; const leaderLabel = String(leadSignal?.leader_station_label || "").trim() || String(leadSignal?.leader_station_code || "").trim(); const leaderSyncStatus = String(leadSignal?.leader_sync_status || "") .trim() .toLowerCase(); const leaderSyncDelta = Number( leadSignal?.leader_time_delta_vs_anchor_minutes, ); const syncNote = leaderSyncStatus === "near_realtime" || leaderSyncStatus === "lagged" ? Number.isFinite(leaderSyncDelta) ? locale === "en-US" ? ` Timing offset versus the airport anchor is about ${Math.round(leaderSyncDelta)} minutes.` : ` 与机场锚点存在约 ${Math.round(leaderSyncDelta)} 分钟时间差。` : locale === "en-US" ? " Nearby observations are not fully synchronized." : " 周边观测并非完全同步。" : leaderSyncStatus === "unknown" ? locale === "en-US" ? " Nearby station timing is not fully verified." : " 周边站观测时间尚未完全校验。" : ""; const absDelta = Math.abs(delta); const status = delta <= -0.4 ? locale === "en-US" ? "Airport trailing" : "机场落后" : delta >= 0.4 ? locale === "en-US" ? "Airport leading" : "机场领先" : locale === "en-US" ? "Tracking network" : "与站网齐平"; const tone = delta <= -0.4 ? "amber" : delta >= 0.4 ? "cyan" : "blue"; const note = delta <= -0.4 ? locale === "en-US" ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} cooler than the nearby official network${leaderLabel ? `, led by ${leaderLabel}` : ""}.${syncNote}` : `机场主站当前比周边官方站网低 ${absDelta.toFixed(1)}${detail.temp_symbol}${leaderLabel ? `,领先点位是 ${leaderLabel}` : ""}。${syncNote}` : delta >= 0.4 ? locale === "en-US" ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} hotter than the nearby official network.${syncNote}` : `机场主站当前比周边官方站网高 ${absDelta.toFixed(1)}${detail.temp_symbol}。${syncNote}` : locale === "en-US" ? "Airport anchor and nearby official network are broadly aligned." : "机场主站与周边官方站网当前大体齐平。"; return { label: locale === "en-US" ? "Airport vs network" : "机场 vs 周边站", note, status, tone, value: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`, }; }, [ detail.airport_vs_network_delta, detail.network_lead_signal, detail.temp_symbol, isToday, locale, showDeferredTodaySections, ]); const paceSignalItems = useMemo( () => [boundaryRiskView, peakWindowStateView, networkLeadView] .filter((item) => item != null) .map((item) => ({ label: item.label, note: item.note, status: item.status, tone: item.tone, value: item.value, })) as FuturePaceSignalItem[], [boundaryRiskView, networkLeadView, peakWindowStateView], ); const isNoaaSettlement = detail.current?.settlement_source === "noaa" || detail.current?.settlement_source_label === "NOAA"; const noaaStationCode = String( detail.current?.station_code || detail.risk?.icao || "NOAA", ) .trim() .toUpperCase(); const noaaStationName = String(detail.current?.station_name || "").trim() || String(detail.risk?.airport || "").trim() || noaaStationCode; const hottestBucketLabel = formatBucketLabel(topProbabilityBucket); const probabilitySummary = (() => { if (!topProbabilityBucket) { return locale === "en-US" ? "Probability mass is still too dispersed; avoid over-reading a single bracket." : "当前概率还比较分散,不要只盯单一区间。"; } const bucketLabel = formatBucketLabel(topProbabilityBucket); const bucketProb = formatMarketPercent(topProbabilityBucket.probability); if (!isToday) { return locale === "en-US" ? `Target-day model probability reference puts the leading bucket at ${bucketLabel} (${bucketProb}). EMOS is reserved for intraday analysis after live anchor observations arrive.` : `目标日模型概率参考显示领先温度桶为 ${bucketLabel}(${bucketProb})。EMOS 仅用于有实时锚点观测后的日内分析。`; } if (hasLgbmProbability) { return locale === "en-US" ? `LGBM-calibrated read puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.` : `LGBM 校准后领先温度桶为 ${bucketLabel}(${bucketProb})。可作为基准情形,但不要直接等同于最终结算。`; } if (hasEmosProbability) { return locale === "en-US" ? `EMOS-calibrated probability puts the leading bucket at ${bucketLabel} (${bucketProb}). It is the primary calibrated probability layer, not the final settlement.` : `EMOS 校准概率显示领先温度桶为 ${bucketLabel}(${bucketProb})。这是当前主概率层,但不要直接等同于最终结算。`; } return locale === "en-US" ? `Calibrated model probability puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.` : `校准模型概率显示领先温度桶为 ${bucketLabel}(${bucketProb})。可作为基准情形,但不要直接等同于最终结算。`; })(); const modelSummary = (() => { if (!modelSpreadView) { return locale === "en-US" ? "Model spread is unavailable right now." : "当前拿不到可用的模型分歧。"; } const modelEntries = Object.entries(modelView?.models || {}).filter( ([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)), ); if (modelEntries.length === 1) { const [singleModelName, singleModelValue] = modelEntries[0]; return locale === "en-US" ? `Only ${singleModelName} is available right now at ${Number(singleModelValue).toFixed(1)}${detail.temp_symbol}; multi-model spread is temporarily unavailable.` : `当前只收到 ${singleModelName} ${Number(singleModelValue).toFixed(1)}${detail.temp_symbol},其他多模型暂未回传,所以这里先不判断模型分歧。`; } return locale === "en-US" ? `Model range runs from ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} to ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol}; spread ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}.` : `当前模型区间在 ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} 到 ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol},分歧 ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}。`; })(); const upperAirSignal = detail.vertical_profile_signal || {}; const tafSignal = detail.taf?.signal || {}; const upperAirCue = useMemo(() => { if (!showDeferredTodaySections) return null; if (!isToday || (!upperAirSignal.source && !tafSignal.available)) return null; const setup = String( upperAirSignal.heating_setup || "neutral", ).toLowerCase(); const tafSuppression = String( tafSignal.suppression_level || "low", ).toLowerCase(); const tafDisruption = String( tafSignal.disruption_level || "low", ).toLowerCase(); const reasons: string[] = []; let score = 0; if (setup === "supportive") { score += 2; reasons.push( locale === "en-US" ? "upper-air structure still supports daytime heating" : "高空结构仍偏支持白天冲高", ); } else if (setup === "suppressed") { score -= 2; reasons.push( locale === "en-US" ? "upper-air structure still leans toward capping the peak" : "高空结构更偏向压住峰值", ); } if (tafSuppression === "high") { score -= 2; reasons.push( locale === "en-US" ? "TAF flags meaningful cloud/rain suppression near the peak window" : "TAF 在峰值窗口提示云雨压温风险偏高", ); } else if (tafSuppression === "medium") { score -= 1; reasons.push( locale === "en-US" ? "TAF keeps some cloud/rain suppression risk on the table" : "TAF 仍提示一定的云雨压温风险", ); } if (tafDisruption === "high") { score -= 1; reasons.push( locale === "en-US" ? "TAF also suggests a noisier afternoon regime" : "TAF 还提示午后扰动偏强", ); } else if (tafDisruption === "medium") { score -= 0.5; reasons.push( locale === "en-US" ? "TAF keeps some afternoon timing noise in play" : "TAF 提示午后仍可能有时段性扰动", ); } if (score >= 1.5) { return { summary: locale === "en-US" ? "The combined upper-air and TAF read still leans warmer. Do not fade lower buckets too early." : "高空和 TAF 两层信号合并后仍偏暖侧,不宜过早做更低温区间。", note: locale === "en-US" ? `${reasons.slice(0, 2).join("; ")}.` : `${reasons.slice(0, 2).join(";")}。`, tone: "warm", value: locale === "en-US" ? "Lean warmer" : "偏暖侧", }; } if (score <= -1.5) { return { summary: locale === "en-US" ? "The combined upper-air and TAF read leans more defensive. Be more careful chasing higher buckets." : "高空和 TAF 两层信号合并后更偏防守,追更高温区间要更谨慎。", note: locale === "en-US" ? `${reasons.slice(0, 2).join("; ")}.` : `${reasons.slice(0, 2).join(";")}。`, tone: "cold", value: locale === "en-US" ? "Lean cautious" : "偏谨慎", }; } return { summary: locale === "en-US" ? "The combined upper-air and TAF read is mixed. Let surface structure decide before taking a side." : "高空和 TAF 两层信号目前偏混合,先看近地面结构变化,不急着站边。", note: locale === "en-US" ? `${reasons.slice(0, 2).join("; ") || "No clean edge from the upper-air layer alone"}.` : `${reasons.slice(0, 2).join(";") || "单看高空层还没有干净的交易边"}。`, tone: "", value: locale === "en-US" ? "Wait / confirm" : "先观察", }; }, [ tafSignal.available, tafSignal.disruption_level, tafSignal.suppression_level, isToday, locale, upperAirSignal.heating_setup, upperAirSignal.source, showDeferredTodaySections, ]); const topObservedTemp = detail.current?.max_so_far != null ? detail.current.max_so_far : detail.current?.temp; const currentTempText = detail.current?.temp != null ? `${detail.current.temp}${detail.temp_symbol}` : "--"; const daylightProgress = (() => { const now = parseClockMinutes(detail.current?.obs_time); const sunrise = parseClockMinutes(detail.forecast?.sunrise); const sunset = parseClockMinutes(detail.forecast?.sunset); if (now == null || sunrise == null || sunset == null || sunset <= sunrise) { return null; } const percent = clamp(((now - sunrise) / (sunset - sunrise)) * 100, 0, 100); const phase = now < sunrise ? "夜间" : now > sunset ? "已日落" : "白昼进行中"; return { phase, percent, }; })(); const displayedUpperAirSummary = showDeferredTodaySections ? upperAirCue?.summary || view.front.upperAirSummary : ""; const displayedUpperAirMetrics = showDeferredTodaySections ? (view.front.upperAirMetrics || []).map((metric, index) => index === 0 && (metric.label === "Trade cue" || metric.label === "交易动作") && upperAirCue ? { ...metric, note: upperAirCue.note, tone: upperAirCue.tone, value: upperAirCue.value, } : metric, ) : []; const localizedAiCommentaryLines = useMemo(() => { if (!showDeferredTodaySections) return [] as string[]; const commentary = detail.dynamic_commentary || {}; const headline = String( locale === "en-US" ? commentary.headline_en || "" : commentary.headline_zh || "", ).trim(); const bullets = ( locale === "en-US" ? commentary.bullets_en : commentary.bullets_zh ) as string[] | null | undefined; const cleanedBullets = Array.isArray(bullets) ? bullets.map((item) => String(item || "").trim()).filter(Boolean) : []; return [headline, ...cleanedBullets].filter(Boolean).slice(0, 3); }, [detail.dynamic_commentary, locale, showDeferredTodaySections]); const todayTradeSummaryLines = useMemo(() => { if (!showDeferredTodaySections) return [] as string[]; if (!isToday) return [] as string[]; if (localizedAiCommentaryLines.length > 0) { return localizedAiCommentaryLines; } const lines: string[] = []; if (paceView) { const headline = paceView.biasTone === "warm" ? locale === "en-US" ? `Pace is running hot by ${paceView.deltaText}; the day high still leans above the base curve.` : `节奏偏热 ${paceView.deltaText},日高仍偏向落在基础曲线之上。` : paceView.biasTone === "cold" ? locale === "en-US" ? `Pace is trailing by ${paceView.deltaText}; chasing higher buckets needs caution.` : `节奏落后 ${paceView.deltaText},继续追更高温区间要更谨慎。` : locale === "en-US" ? "Pace is still on curve; the next move depends on the peak-window push." : "节奏目前贴着曲线走,下一步主要看峰值窗口还有没有上冲。"; lines.push(headline); } if (boundaryRiskView) { lines.push( locale === "en-US" ? `${boundaryRiskView.label}: ${boundaryRiskView.note}` : `${boundaryRiskView.label}:${boundaryRiskView.note}`, ); } if (networkLeadView) { lines.push( locale === "en-US" ? `${networkLeadView.label}: ${networkLeadView.note}` : `${networkLeadView.label}:${networkLeadView.note}`, ); } return lines.slice(0, 3); }, [ boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView, showDeferredTodaySections, ]); const intradayMeteorology = detail.intraday_meteorology || {}; const meteorologySignals = Array.isArray( intradayMeteorology.signal_contributions, ) ? intradayMeteorology.signal_contributions : []; const invalidationRules = localizedList( locale, intradayMeteorology.invalidation_rules, intradayMeteorology.invalidation_rules_en, ); const confirmationRules = localizedList( locale, intradayMeteorology.confirmation_rules, intradayMeteorology.confirmation_rules_en, ); const meteorologyHeadline = localizedText( locale, intradayMeteorology.headline, intradayMeteorology.headline_en, ) || todayTradeSummaryLines[0] || (locale === "en-US" ? "Intraday meteorology layers are still syncing; use the next observation as the anchor." : "关键日内气象层仍在同步,先以下一次观测作为判断锚点。"); const baseCaseBucket = String(intradayMeteorology.base_case_bucket || "").trim() || formatBucketLabel(topProbabilityBucket); const nextObservationTime = String(intradayMeteorology.next_observation_time || "").trim() || "--"; const baseBucketNumber = parseLeadingNumber(baseCaseBucket); const referenceObservedTemp = topObservedTemp != null && Number.isFinite(Number(topObservedTemp)) ? Number(topObservedTemp) : detail.current?.temp != null ? Number(detail.current.temp) : null; const gapToBaseBucket = baseBucketNumber != null && referenceObservedTemp != null ? Math.max(0, baseBucketNumber - referenceObservedTemp) : null; const pathStatus = gapToBaseBucket == null ? locale === "en-US" ? "Awaiting anchor" : "等待锚点" : gapToBaseBucket <= 0.05 ? locale === "en-US" ? "Base path touched" : "基准路径已触达" : gapToBaseBucket <= 1.0 ? locale === "en-US" ? "Base path open" : "基准路径开放" : locale === "en-US" ? "Needs peak push" : "需要峰值推动"; const peakWindowText = String(intradayMeteorology.peak_window || "").trim() || paceView?.peakWindowText || "--"; const settlementSourceCode = normalizeObservationSourceCode( detail.current?.settlement_source || "", ); 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" || Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode)); const anchorSourceLabel = airportMetarAnchor ? settlementStationCode ? `${settlementStationCode} METAR` : "METAR" : 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. Third-party history pages are display-only when present.` : `机场合约锚点:以 ${anchorSourceLabel} 报文为准;第三方历史页只作为展示入口。` : 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", state: isAnyLayerSyncing ? "syncing" : "ready", label: isAnyLayerSyncing ? locale === "en-US" ? "Refreshing base analysis" : "正在刷新基础分析" : locale === "en-US" ? "Base analysis ready" : "基础分析已加载", note: isAnyLayerSyncing ? locale === "en-US" ? "Latest anchor readings and forecast curve are being rebuilt." : "正在重建最新锚点读数和预测曲线。" : locale === "en-US" ? "Forecast curve, anchor state, and the core intraday view are available." : "预测曲线、锚点状态和核心日内视图已经可用。", }, { key: "market", state: isAnyLayerSyncing ? "syncing" : "ready", label: isAnyLayerSyncing ? locale === "en-US" ? "Refreshing probability layer" : "正在刷新概率层" : locale === "en-US" ? "Probability layer ready" : "概率层已加载", note: isAnyLayerSyncing ? locale === "en-US" ? "Model spread and calibrated buckets are updating." : "模型分歧和校准概率桶正在更新。" : locale === "en-US" ? `Probability buckets are derived from the ${probabilityEngineLabel} layer.` : `概率桶当前由 ${probabilityEngineLabel} 层推导。`, }, ] satisfies FutureSyncStatusItem[]; return (
{ if (event.target === event.currentTarget) { modal.closeFutureModal(); } }} > {isProLoading ? (
{t("dashboard.loading")}
) : !isPro ? ( ) : (
{ if (isToday) { void modal.openTodayModal(true); return; } modal.openFutureModal(dateStr, true); }} t={t} />
{isTodayBlockingRefresh && } {isToday && ( )} {isNoaaSettlement && (
{locale === "en-US" ? `${detail.display_name} now settles against NOAA ${noaaStationCode} (${noaaStationName}). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized.` : `${detail.display_name} 当前按 NOAA ${noaaStationCode}(${noaaStationName})结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。`}
)} {isToday ? ( ) : ( )}
)}
); }