From baaccc663657943c60f80dd073cfec17d95ebd9f Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 13 Apr 2026 18:42:27 +0800 Subject: [PATCH] feat: implement multi-source weather data collection system and dashboard frontend with integrated analysis services. --- data/probability_training_snapshots.jsonl | 8 + frontend/components/dashboard/DetailPanel.tsx | 14 +- .../dashboard/FutureForecastModal.tsx | 401 +++++++++++------- .../components/dashboard/HistoryModal.tsx | 21 +- .../dashboard/PolyWeatherDashboard.tsx | 57 ++- frontend/hooks/useChart.ts | 11 +- frontend/hooks/useDashboardStore.tsx | 164 ++++++- frontend/lib/dashboard-client.ts | 37 +- frontend/lib/dashboard-types.ts | 23 + src/data_collection/weather_sources.py | 6 +- web/analysis_service.py | 50 ++- web/routes.py | 44 +- 12 files changed, 624 insertions(+), 212 deletions(-) diff --git a/data/probability_training_snapshots.jsonl b/data/probability_training_snapshots.jsonl index b74f0633..ecc6d6bd 100644 --- a/data/probability_training_snapshots.jsonl +++ b/data/probability_training_snapshots.jsonl @@ -169,3 +169,11 @@ {"city": "hong kong", "timestamp": "2026-04-12T10:00:00+08:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 28.999999999999996, "raw_sigma": 0.75, "deb_prediction": 28.2, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 27.5, "HKO(港天文)": 29.0}, "max_so_far": 27.2, "observation": {"current_temp": 27.2, "humidity": 77.0, "wind_speed_kt": 5.4, "visibility_mi": null, "local_hour": 10.283333333333333}, "peak_status": "before", "prob_snapshot": [{"v": 28, "p": 0.412}, {"v": 29, "p": 0.412}, {"v": 27, "p": 0.088}, {"v": 30, "p": 0.088}], "shadow_prob_snapshot": [{"v": 28, "p": 0.413}, {"v": 29, "p": 0.413}, {"v": 27, "p": 0.087}, {"v": 30, "p": 0.087}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 28.999999999999996, "calibrated_sigma": 0.7478013193453208} {"city": "taipei", "timestamp": "2026-04-12T10:00:00+08:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 30.689999999999998, "raw_sigma": 2.8574999999999995, "deb_prediction": 30.9, "ensemble": {"p10": 29.7, "median": 30.2, "p90": 30.6}, "multi_model": {"Open-Meteo": 30.9, "ECMWF": 30.9, "GFS": 32.9, "ICON": 30.9, "GEM": 28.9, "JMA": 30.8}, "max_so_far": 29.2, "observation": {"current_temp": 29.2, "humidity": 64.0, "wind_speed_kt": 1.2, "visibility_mi": null, "local_hour": 10.283333333333333}, "peak_status": "before", "prob_snapshot": [{"v": 31, "p": 0.179}, {"v": 30, "p": 0.175}, {"v": 32, "p": 0.163}, {"v": 29, "p": 0.152}], "shadow_prob_snapshot": [{"v": 31, "p": 0.183}, {"v": 30, "p": 0.178}, {"v": 32, "p": 0.165}, {"v": 29, "p": 0.153}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 30.689999999999998, "calibrated_sigma": 2.778122088606427} {"city": "hong kong", "timestamp": "2026-04-12T10:00:00+08:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 27.71, "raw_sigma": 0.30937500000000073, "deb_prediction": 27.6, "ensemble": {"p10": 28.1, "median": 28.2, "p90": 28.5}, "multi_model": {"Open-Meteo": 27.5, "HKO(港天文)": 29.0, "ECMWF": 27.0, "GFS": 27.2, "ICON": 27.5, "GEM": 27.1, "JMA": 27.6}, "max_so_far": 27.2, "observation": {"current_temp": 27.2, "humidity": 77.0, "wind_speed_kt": 5.4, "visibility_mi": null, "local_hour": 10.283333333333333}, "peak_status": "before", "prob_snapshot": [{"v": 27, "p": 0.824}, {"v": 28, "p": 0.176}], "shadow_prob_snapshot": [{"v": 27, "p": 0.814}, {"v": 28, "p": 0.186}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 27.71, "calibrated_sigma": 0.32103944874938267} +{"city": "ankara", "timestamp": "2026-04-13T10:20:00.000Z", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": 10.8, "raw_sigma": 2.5, "deb_prediction": 10.9, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 10.6, "ECMWF": 11.7, "GFS": 11.3, "ICON": 10.6, "GEM": 10.8, "JMA": 10.5}, "max_so_far": 9.0, "observation": {"current_temp": 9.0, "humidity": null, "wind_speed_kt": 6.0, "visibility_mi": null, "local_hour": 13.566666666666666}, "peak_status": "before", "prob_snapshot": [{"v": 11, "p": 0.195}, {"v": 10, "p": 0.186}, {"v": 12, "p": 0.175}, {"v": 9, "p": 0.152}], "shadow_prob_snapshot": [{"v": 11, "p": 0.199}, {"v": 10, "p": 0.189}, {"v": 12, "p": 0.177}, {"v": 9, "p": 0.152}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 10.8, "calibrated_sigma": 2.4191943428283595} +{"city": "ankara", "timestamp": "2026-04-13T10:20:00.000Z", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": 10.77, "raw_sigma": 3.7124999999999995, "deb_prediction": 10.9, "ensemble": {"p10": 9.8, "median": 10.7, "p90": 11.6}, "multi_model": {"Open-Meteo": 10.6, "ECMWF": 11.7, "GFS": 11.3, "ICON": 10.6, "GEM": 10.8, "JMA": 10.5}, "max_so_far": 9.0, "observation": {"current_temp": 9.0, "humidity": null, "wind_speed_kt": 6.0, "visibility_mi": null, "local_hour": 13.583333333333334}, "peak_status": "before", "prob_snapshot": [{"v": 11, "p": 0.15}, {"v": 10, "p": 0.148}, {"v": 12, "p": 0.143}, {"v": 9, "p": 0.135}], "shadow_prob_snapshot": [{"v": 11, "p": 0.173}, {"v": 10, "p": 0.168}, {"v": 12, "p": 0.16}, {"v": 9, "p": 0.146}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 10.77, "calibrated_sigma": 3.0} +{"city": "istanbul", "timestamp": "2026-04-13T13:20:00+03:00", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": 12.5, "raw_sigma": 1.7999999999999998, "deb_prediction": 12.2, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 12.5, "ECMWF": 12.4, "GFS": 12.3, "ICON": 12.5, "GEM": 13.5, "JMA": 10.4}, "max_so_far": 12.0, "observation": {"current_temp": 12.0, "humidity": 46.9, "wind_speed_kt": 14.0, "visibility_mi": null, "local_hour": 13.583333333333334}, "peak_status": "before", "prob_snapshot": [{"v": 12, "p": 0.298}, {"v": 13, "p": 0.298}, {"v": 14, "p": 0.22}, {"v": 15, "p": 0.121}], "shadow_prob_snapshot": [{"v": 12, "p": 0.294}, {"v": 13, "p": 0.294}, {"v": 14, "p": 0.22}, {"v": 15, "p": 0.124}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 12.5, "calibrated_sigma": 1.8402601718515887} +{"city": "hong kong", "timestamp": "2026-04-13T18:20:00+08:00", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.47999999999999987, "deb_prediction": 27.5, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 26.8, "HKO(港天文)": 30.0, "ECMWF": 27.5, "GFS": 26.9, "ICON": 26.8, "GEM": 27.7, "JMA": 28.1}, "max_so_far": 29.7, "observation": {"current_temp": 27.5, "humidity": 76.0, "wind_speed_kt": 4.3, "visibility_mi": null, "local_hour": 18.583333333333332}, "peak_status": "past", "prob_snapshot": [{"v": 29, "p": 1.0}], "shadow_prob_snapshot": [], "probability_engine": "legacy", "probability_mode": "legacy", "calibration_version": null, "calibration_source": null, "calibrated_mu": null, "calibrated_sigma": null} +{"city": "hong kong", "timestamp": "2026-04-13T18:20:00+08:00", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.10952569444444488, "deb_prediction": 27.5, "ensemble": {"p10": 28.0, "median": 28.3, "p90": 28.6}, "multi_model": {"Open-Meteo": 26.8, "HKO(港天文)": 30.0, "ECMWF": 27.5, "GFS": 26.9, "ICON": 26.8, "GEM": 27.7, "JMA": 28.1}, "max_so_far": 29.7, "observation": {"current_temp": 27.5, "humidity": 76.0, "wind_speed_kt": 4.3, "visibility_mi": null, "local_hour": 18.583333333333332}, "peak_status": "past", "prob_snapshot": [{"v": 29, "p": 1.0}], "shadow_prob_snapshot": [], "probability_engine": "legacy", "probability_mode": "legacy", "calibration_version": null, "calibration_source": null, "calibrated_mu": null, "calibrated_sigma": null} +{"city": "tokyo", "timestamp": "2026-04-13T10:00:00.000Z", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.4349999999999998, "deb_prediction": 21.9, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 20.3, "ECMWF": 22.8, "GFS": 22.0, "ICON": 22.6, "GEM": 23.2, "JMA": 20.3}, "max_so_far": 23.0, "observation": {"current_temp": 20.0, "humidity": null, "wind_speed_kt": 10.0, "visibility_mi": null, "local_hour": 19.583333333333332}, "peak_status": "past", "prob_snapshot": [{"v": 23, "p": 1.0}], "shadow_prob_snapshot": [], "probability_engine": "legacy", "probability_mode": "legacy", "calibration_version": null, "calibration_source": null, "calibrated_mu": null, "calibrated_sigma": null} +{"city": "hong kong", "timestamp": "2026-04-13T18:30:00+08:00", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.47999999999999987, "deb_prediction": 27.5, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 26.8, "HKO(港天文)": 30.0, "ECMWF": 27.5, "GFS": 26.9, "ICON": 26.8, "GEM": 27.7, "JMA": 28.1}, "max_so_far": 29.7, "observation": {"current_temp": 27.4, "humidity": 77.0, "wind_speed_kt": 2.2, "visibility_mi": null, "local_hour": 18.666666666666668}, "peak_status": "past", "prob_snapshot": [{"v": 29, "p": 1.0}], "shadow_prob_snapshot": [], "probability_engine": "legacy", "probability_mode": "legacy", "calibration_version": null, "calibration_source": null, "calibrated_mu": null, "calibrated_sigma": null} +{"city": "hong kong", "timestamp": "2026-04-13T18:30:00+08:00", "date": "2026-04-13", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.10952569444444488, "deb_prediction": 27.5, "ensemble": {"p10": 28.0, "median": 28.3, "p90": 28.6}, "multi_model": {"Open-Meteo": 26.8, "HKO(港天文)": 30.0, "ECMWF": 27.5, "GFS": 26.9, "ICON": 26.8, "GEM": 27.7, "JMA": 28.1}, "max_so_far": 29.7, "observation": {"current_temp": 27.4, "humidity": 77.0, "wind_speed_kt": 2.2, "visibility_mi": null, "local_hour": 18.666666666666668}, "peak_status": "past", "prob_snapshot": [{"v": 29, "p": 1.0}], "shadow_prob_snapshot": [], "probability_engine": "legacy", "probability_mode": "legacy", "calibration_version": null, "calibration_source": null, "calibrated_mu": null, "calibrated_sigma": null} diff --git a/frontend/components/dashboard/DetailPanel.tsx b/frontend/components/dashboard/DetailPanel.tsx index 947a661c..24161a0c 100644 --- a/frontend/components/dashboard/DetailPanel.tsx +++ b/frontend/components/dashboard/DetailPanel.tsx @@ -5,7 +5,7 @@ import clsx from "clsx"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { ForecastTable } from "@/components/dashboard/PanelSections"; -import { useChart } from "@/hooks/useChart"; +import { preloadChartJs, useChart } from "@/hooks/useChart"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; import { getOfficialSourceLinks } from "@/lib/dashboard-official-sources"; @@ -360,6 +360,12 @@ export function DetailPanel() { : `${t("detail.todayAnalysis")} (Pro)` } onClick={() => handleFeatureAccess("today")} + onFocus={() => { + void preloadChartJs(); + }} + onMouseEnter={() => { + void preloadChartJs(); + }} disabled={!store.selectedCity} > {isPro @@ -373,6 +379,12 @@ export function DetailPanel() { isPro ? t("detail.history") : `${t("detail.history")} (Pro)` } onClick={() => handleFeatureAccess("history")} + onFocus={() => { + void preloadChartJs(); + }} + onMouseEnter={() => { + void preloadChartJs(); + }} disabled={!store.selectedCity} > {isPro ? t("detail.history") : `${t("detail.history")} · Pro`} diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index 2e142a2f..2e5cd246 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -14,7 +14,7 @@ import { import type { ChartConfiguration } from "chart.js"; import clsx from "clsx"; -import { CSSProperties, useMemo } from "react"; +import { CSSProperties, useEffect, useMemo, useState } from "react"; import { useChart } from "@/hooks/useChart"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; @@ -626,13 +626,47 @@ export function FutureForecastModal() { const dateStr = store.futureModalDate; const isPro = store.proAccess.subscriptionActive; const isProLoading = store.proAccess.loading; + const [showDeferredTodaySections, setShowDeferredTodaySections] = useState(false); if (!detail || !dateStr) return null; + useEffect(() => { + 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]); + const isToday = dateStr === detail.local_date; const detailDepth = detail.detail_depth || "full"; const isFullDetailReady = detailDepth === "full"; - const isStructureSyncing = store.loadingState.refresh || !isFullDetailReady; + const isStructureSyncing = store.loadingState.futureDeep || !isFullDetailReady; const isMarketSyncing = store.loadingState.marketScan; const isAnyLayerSyncing = isStructureSyncing || isMarketSyncing; const view = getFutureModalView(detail, dateStr, locale); @@ -642,8 +676,11 @@ export function FutureForecastModal() { } as CSSProperties & { "--score-position": string }; const weatherSummary = getWeatherSummary(detail, locale); const paceView = useMemo( - () => (isToday ? getTodayPaceView(detail, locale) : null), - [detail, isToday, locale], + () => + isToday && showDeferredTodaySections + ? getTodayPaceView(detail, locale) + : null, + [detail, isToday, locale, showDeferredTodaySections], ); const probabilityView = useMemo( () => getProbabilityView(detail, dateStr), @@ -674,6 +711,7 @@ export function FutureForecastModal() { }; }, [modelView]); const boundaryRiskView = useMemo(() => { + if (!showDeferredTodaySections) return null; if (!isToday || !paceView) return null; const selectedBucket = marketScan?.temperature_bucket || null; const bounds = parseBucketBoundaries(selectedBucket); @@ -718,8 +756,9 @@ export function FutureForecastModal() { tone, value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`, }; - }, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, marketScan?.temperature_bucket, paceView]); + }, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, marketScan?.temperature_bucket, paceView, showDeferredTodaySections]); 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); @@ -754,8 +793,9 @@ export function FutureForecastModal() { tone, value: paceView.peakWindowText, }; - }, [detail.local_time, detail.peak?.first_h, detail.peak?.last_h, isToday, locale, paceView]); + }, [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; @@ -797,7 +837,7 @@ export function FutureForecastModal() { tone, value: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`, }; - }, [detail.airport_vs_network_delta, detail.network_lead_signal, detail.temp_symbol, isToday, locale]); + }, [detail.airport_vs_network_delta, detail.network_lead_signal, detail.temp_symbol, isToday, locale, showDeferredTodaySections]); const isNoaaSettlement = detail.current?.settlement_source === "noaa" || detail.current?.settlement_source_label === "NOAA"; @@ -871,6 +911,7 @@ export function FutureForecastModal() { formatBucketLabel(marketScan?.temperature_bucket) !== "--" && hottestBucketLabel === formatBucketLabel(marketScan?.temperature_bucket); const marketAwareUpperAirCue = useMemo(() => { + if (!showDeferredTodaySections) return null; if (!isToday || (!upperAirSignal.source && !tafSignal.available)) return null; const crowded = hottestMatchesSettlement && (topBucketProbability || 0) >= 0.3; @@ -1014,6 +1055,7 @@ export function FutureForecastModal() { topBucketProbability, upperAirSignal.heating_setup, upperAirSignal.source, + showDeferredTodaySections, ]); const topObservedTemp = detail.current?.max_so_far != null @@ -1038,22 +1080,25 @@ export function FutureForecastModal() { percent, }; })(); - const displayedUpperAirSummary = - marketAwareUpperAirCue?.summary || view.front.upperAirSummary; - const displayedUpperAirMetrics = (view.front.upperAirMetrics || []).map( - (metric, index) => - index === 0 && - (metric.label === "Trade cue" || metric.label === "交易动作") && - marketAwareUpperAirCue - ? { - ...metric, - note: marketAwareUpperAirCue.note, - tone: marketAwareUpperAirCue.tone, - value: marketAwareUpperAirCue.value, - } - : metric, - ); + const displayedUpperAirSummary = showDeferredTodaySections + ? marketAwareUpperAirCue?.summary || view.front.upperAirSummary + : ""; + const displayedUpperAirMetrics = showDeferredTodaySections + ? (view.front.upperAirMetrics || []).map((metric, index) => + index === 0 && + (metric.label === "Trade cue" || metric.label === "交易动作") && + marketAwareUpperAirCue + ? { + ...metric, + note: marketAwareUpperAirCue.note, + tone: marketAwareUpperAirCue.tone, + value: marketAwareUpperAirCue.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 || "", @@ -1065,8 +1110,9 @@ export function FutureForecastModal() { ? bullets.map((item) => String(item || "").trim()).filter(Boolean) : []; return [headline, ...cleanedBullets].filter(Boolean).slice(0, 3); - }, [detail.dynamic_commentary, locale]); + }, [detail.dynamic_commentary, locale, showDeferredTodaySections]); const todayTradeSummaryLines = useMemo(() => { + if (!showDeferredTodaySections) return [] as string[]; if (!isToday) return [] as string[]; if (localizedAiCommentaryLines.length > 0) { return localizedAiCommentaryLines; @@ -1102,7 +1148,7 @@ export function FutureForecastModal() { ); } return lines.slice(0, 3); - }, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView]); + }, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView, showDeferredTodaySections]); const syncStatusItems = [ { key: "base", @@ -1379,7 +1425,7 @@ export function FutureForecastModal() { - {paceView ? ( + {showDeferredTodaySections && paceView ? (

@@ -1497,6 +1543,24 @@ export function FutureForecastModal() { ))}

+ ) : isToday ? ( +
+
+

+ {locale === "en-US" ? "Current Pace" : "当前节奏"} +

+
+ {locale === "en-US" + ? "Backfilling intraday pace context" + : "正在补齐日内节奏上下文"} +
+
+
+ {locale === "en-US" + ? "Expected-now pace, boundary risk, and airport-vs-network cues are loading in the background." + : "预期此刻节奏、边界风险和机场对比站网信号正在后台补齐。"} +
+
) : null} @@ -1560,164 +1624,175 @@ export function FutureForecastModal() { -
-

{t("future.structureToday")}

-
-
-
-
-
- - {t("future.judgement")}: {view.front.label} - - - {t("future.confidence")}:{" "} - {t(`confidence.${view.front.confidence}`)} - - - {t("future.maxPrecip")}:{" "} - {Math.round(view.front.precipMax)}% - -
- {todayTradeSummaryLines.length > 0 ? ( -
- {todayTradeSummaryLines.map((line, index) => ( -
{line}
- ))} -
- ) : null} -
-
- {locale === "en-US" ? "Surface Structure" : "近地面信号"} -
-
- {view.front.metrics.slice(0, 6).map((metric) => ( -
-
{metric.label}
+ {showDeferredTodaySections ? ( +
+

{t("future.structureToday")}

+
+
- {metric.value} + style={{ + position: "absolute", + top: 0, + bottom: 0, + left: "50%", + width: "2px", + background: "rgba(255, 255, 255, 0.2)", + transform: "translateX(-50%)", + zIndex: 1, + }} + /> +
+
+ + {t("future.judgement")}: {view.front.label} + + + {t("future.confidence")}:{" "} + {t(`confidence.${view.front.confidence}`)} + + + {t("future.maxPrecip")}:{" "} + {Math.round(view.front.precipMax)}% + +
+ {todayTradeSummaryLines.length > 0 ? ( +
+ {todayTradeSummaryLines.map((line, index) => ( +
{line}
+ ))}
- {getTrendMetricVisual(metric) ? ( + ) : null} +
+
+ {locale === "en-US" ? "Surface Structure" : "近地面信号"} +
+
+ {view.front.metrics.slice(0, 6).map((metric) => ( +
+
{metric.label}
- {getTrendMetricVisual(metric)?.mode === "center" ? ( - - ) : null} -
+ {metric.value}
- ) : null} -
{metric.note}
-
- ))} -
- <> -
- {locale === "en-US" ? "Upper-Air Structure" : "高空结构信号"} -
- {displayedUpperAirSummary ? ( -
- {displayedUpperAirSummary} -
- ) : ( -
- {locale === "en-US" - ? "Upper-air structure is temporarily unavailable for this city. For now, lean on surface structure and TAF timing." - : "该城市当前暂无可用的高空结构数据,先以近地面结构和 TAF 时段作为主判断。"} -
- )} - {displayedUpperAirMetrics.length > 0 ? ( -
- {displayedUpperAirMetrics.map((metric) => ( -
-
{metric.label}
+ {getTrendMetricVisual(metric) ? (
- {metric.value} -
- {getTrendMetricVisual(metric) ? ( + {getTrendMetricVisual(metric)?.mode === "center" ? ( + + ) : null}
+
+ ) : null} +
{metric.note}
+
+ ))} +
+ <> +
+ {locale === "en-US" ? "Upper-Air Structure" : "高空结构信号"} +
+ {displayedUpperAirSummary ? ( +
+ {displayedUpperAirSummary} +
+ ) : ( +
+ {locale === "en-US" + ? "Upper-air structure is temporarily unavailable for this city. For now, lean on surface structure and TAF timing." + : "该城市当前暂无可用的高空结构数据,先以近地面结构和 TAF 时段作为主判断。"} +
+ )} + {displayedUpperAirMetrics.length > 0 ? ( +
+ {displayedUpperAirMetrics.map((metric) => ( +
+
{metric.label}
+
- {getTrendMetricVisual(metric)?.mode === "center" ? ( - - ) : null} + {metric.value} +
+ {getTrendMetricVisual(metric) ? (
-
- ) : null} -
{metric.note}
+ > + {getTrendMetricVisual(metric)?.mode === "center" ? ( + + ) : null} +
+
+ ) : null} +
{metric.note}
+
+ ))} +
+ ) : ( +
+
+ {locale === "en-US" ? "Upper-air source" : "高空数据源"} +
+
+ {locale === "en-US" ? "Not available" : "暂不可用"} +
+
+ {locale === "en-US" + ? "No upper-air diagnostic feed is attached to this city right now." + : "当前该城市未接入可用的高空诊断源,所以这里先保留说明卡片。"}
- ))} -
- ) : ( -
-
- {locale === "en-US" ? "Upper-air source" : "高空数据源"}
-
- {locale === "en-US" ? "Not available" : "暂不可用"} -
-
- {locale === "en-US" - ? "No upper-air diagnostic feed is attached to this city right now." - : "当前该城市未接入可用的高空诊断源,所以这里先保留说明卡片。"} -
-
- )} - -
+ )} + +
+ ) : ( +
+

{t("future.structureToday")}

+
+ {locale === "en-US" + ? "Surface structure, upper-air diagnostics, and trade commentary are loading after the primary chart." + : "近地面结构、高空诊断和交易提示会在主图之后继续后台补齐。"} +
+
+ )} ) : ( diff --git a/frontend/components/dashboard/HistoryModal.tsx b/frontend/components/dashboard/HistoryModal.tsx index 9714ea88..c5c76e44 100644 --- a/frontend/components/dashboard/HistoryModal.tsx +++ b/frontend/components/dashboard/HistoryModal.tsx @@ -166,7 +166,7 @@ function HistoryChart() { export function HistoryModal() { const store = useDashboardStore(); const { t, locale } = useI18n(); - const { data, error, isLoading, isOpen } = useHistoryData(); + const { data, error, isLoading, isOpen, isRecordsLoading, meta } = useHistoryData(); const isPro = store.proAccess.subscriptionActive; const isProLoading = store.proAccess.loading; const isNoaaSettlement = @@ -233,6 +233,25 @@ export function HistoryModal() { city: store.selectedCity?.toUpperCase() || "", })} + {meta?.mode === "preview" ? ( +
+ {isRecordsLoading + ? locale === "en-US" + ? "Loading full records in background..." + : "完整历史正在后台补齐..." + : meta.hasMore + ? locale === "en-US" + ? `Preview ${meta.previewCount}/${meta.fullCount}` + : `预览 ${meta.previewCount}/${meta.fullCount}` + : null} +
+ ) : null}