From 7b1f34db2732730e3bb7e5663e924b730307b4df Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 8 Apr 2026 15:23:34 +0800 Subject: [PATCH] Refresh sparse model detail caches in forecast views --- data/probability_training_snapshots.jsonl | 1 + .../dashboard/FutureForecastModal.tsx | 10 ++++++ .../components/dashboard/PanelSections.tsx | 14 ++++++++ frontend/hooks/useDashboardStore.tsx | 32 ++++++++++++++++++- frontend/lib/dashboard-client.ts | 13 ++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/data/probability_training_snapshots.jsonl b/data/probability_training_snapshots.jsonl index 48bba991..692e7f87 100644 --- a/data/probability_training_snapshots.jsonl +++ b/data/probability_training_snapshots.jsonl @@ -123,3 +123,4 @@ {"city": "shenzhen", "timestamp": "2026-03-25T10:02:35+00:00", "date": "2026-03-25", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.15911458333333342, "deb_prediction": 28.2, "ensemble": {"p10": 30.5, "median": 31.4, "p90": 31.8}, "multi_model": {"Open-Meteo": 26.5, "ECMWF": 28.8, "GFS": 30.3, "ICON": 26.5, "GEM": 30.7, "JMA": 26.2}, "max_so_far": 28.9, "peak_status": "past", "prob_snapshot": [{"v": 28, "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": "shanghai", "timestamp": "2026-03-29T15:00:00.000Z", "date": "2026-03-29", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.23736458333333335, "deb_prediction": 18.4, "ensemble": {"p10": 16.1, "median": 16.5, "p90": 16.9}, "multi_model": {"Open-Meteo": 17.0, "ECMWF": 19.2, "GFS": 19.1, "ICON": 17.0, "GEM": 17.6, "JMA": 15.8}, "max_so_far": 18.0, "observation": {"current_temp": 14.0, "humidity": null, "wind_speed_kt": 4.0, "visibility_mi": 3.11, "local_hour": 23.25}, "peak_status": "past", "prob_snapshot": [{"v": 18, "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": "ankara", "timestamp": "2026-03-29T15:01:00.000Z", "date": "2026-03-29", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.25176666666666664, "deb_prediction": 9.7, "ensemble": {"p10": 9.2, "median": 9.5, "p90": 10.2}, "multi_model": {"Open-Meteo": 9.2, "ECMWF": 9.5, "GFS": 10.1, "ICON": 9.2, "GEM": 11.0, "JMA": 10.0}, "max_so_far": 10.0, "observation": {"current_temp": 7.0, "humidity": null, "wind_speed_kt": 12.0, "visibility_mi": null, "local_hour": 18.25}, "peak_status": "past", "prob_snapshot": [{"v": 10, "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": "chengdu", "timestamp": "2026-04-08T07:00:00.000Z", "date": "2026-04-08", "temp_symbol": "°C", "raw_mu": 24.3, "raw_sigma": 1.1821289062499998, "deb_prediction": 23.1, "ensemble": {"p10": 21.8, "median": 23.0, "p90": 24.5}, "multi_model": {"Open-Meteo": 22.5, "ECMWF": 23.9, "GFS": 23.0, "ICON": 22.5, "GEM": 23.7, "JMA": 23.2}, "max_so_far": 24.0, "observation": {"current_temp": 24.0, "humidity": null, "wind_speed_kt": 4.0, "visibility_mi": null, "local_hour": 15.183333333333334}, "peak_status": "before", "prob_snapshot": [{"v": 24, "p": 0.442}, {"v": 25, "p": 0.386}, {"v": 26, "p": 0.172}], "shadow_prob_snapshot": [{"v": 24, "p": 0.394}, {"v": 25, "p": 0.355}, {"v": 26, "p": 0.19}, {"v": 27, "p": 0.06}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 24.3, "calibrated_sigma": 1.354078466151897} diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index be6c835a..1c7d9897 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -609,6 +609,7 @@ export function FutureForecastModal() { const max = Math.max(...values); const spread = max - min; return { + count: values.length, max, min, spread, @@ -790,6 +791,15 @@ export function FutureForecastModal() { ? "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}。`; diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 8ed51afe..bff9ddd4 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -609,6 +609,7 @@ export function ModelForecast({ ([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)), ); + const hasSingleModelOnly = modelEntries.length === 1; // 如果没有任何数值,给出提示 if (modelEntries.length === 0) { @@ -637,6 +638,19 @@ export function ModelForecast({
{!hideTitle &&

{t("section.models")}

}
+ {hasSingleModelOnly && ( +
+ {locale === "en-US" + ? "Single-model fallback: waiting for the rest of the model cluster." + : "当前处于单模型回退,其他模型结果还没回传。"} +
+ )} {modelEntries .sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0)) .map(([name, value]) => { diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index 5ba17c3f..003da142 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -91,6 +91,28 @@ const BACKGROUND_SUMMARY_REFRESH_MS = 30_000; const EAGER_CITY_SUMMARIES_ENABLED = process.env.NEXT_PUBLIC_POLYWEATHER_EAGER_CITY_SUMMARIES === "true"; +function countAvailableModels( + detail?: CityDetail | null, + targetDate?: string | null, +): number { + if (!detail) return 0; + const date = String(targetDate || detail.local_date || "").trim(); + const dailyModels = detail.multi_model_daily?.[date]?.models; + const models = dailyModels && typeof dailyModels === "object" + ? dailyModels + : detail.multi_model || {}; + return Object.values(models).filter((value) => + Number.isFinite(Number(value)), + ).length; +} + +function hasSparseModelCoverage( + detail?: CityDetail | null, + targetDate?: string | null, +): boolean { + return countAvailableModels(detail, targetDate) <= 1; +} + export function DashboardStoreProvider({ children, }: { @@ -598,6 +620,12 @@ export function DashboardStoreProvider({ mapStopMotionRef.current(); setFutureModalDate(dateStr); if (!selectedCity || !proAccess.subscriptionActive) return; + const cachedDetail = cityDetailsByName[selectedCity]; + const needsModelRefresh = + !forceRefresh && hasSparseModelCoverage(cachedDetail, dateStr); + if (needsModelRefresh) { + void ensureCityDetail(selectedCity, true).catch(() => {}); + } const cacheKey = getMarketScanCacheKey(selectedCity, dateStr); setLoadingState((current) => ({ ...current, marketScan: true })); void ensureCityMarketScan( @@ -624,6 +652,8 @@ export function DashboardStoreProvider({ setFutureModalDate(cachedDetail.local_date); } if (!proAccess.subscriptionActive) return; + const needsModelRefresh = + !forceRefresh && hasSparseModelCoverage(cachedDetail, cachedDetail?.local_date); setLoadingState((current) => ({ ...current, @@ -634,7 +664,7 @@ export function DashboardStoreProvider({ try { const detail = await ensureCityDetail( selectedCity, - Boolean(forceRefresh), + Boolean(forceRefresh || needsModelRefresh), ); setSelectedForecastDate(detail.local_date); setFutureModalDate(detail.local_date); diff --git a/frontend/lib/dashboard-client.ts b/frontend/lib/dashboard-client.ts index a140f4d0..1f3b7dd0 100644 --- a/frontend/lib/dashboard-client.ts +++ b/frontend/lib/dashboard-client.ts @@ -52,11 +52,24 @@ function normalizeRevisionPart(value: unknown) { export function getCityRevision(source?: CityDetail | CitySummary | null) { if (!source) return ""; + const modelDaily = + "multi_model_daily" in source && source.multi_model_daily + ? source.multi_model_daily?.[source.local_date || ""] + : null; + const modelFootprint = modelDaily?.models || ("multi_model" in source ? source.multi_model : null); return [ normalizeRevisionPart(source.updated_at), normalizeRevisionPart(source.current?.obs_time), normalizeRevisionPart(source.current?.temp), normalizeRevisionPart(source.deb?.prediction), + normalizeRevisionPart( + modelFootprint && typeof modelFootprint === "object" + ? Object.keys(modelFootprint) + .sort() + .map((key) => `${key}:${normalizeRevisionPart(modelFootprint[key])}`) + .join("|") + : "", + ), ].join("|"); }