From 627bb8aa0627dc952bca30e83f85befe1516f9c9 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 12 Apr 2026 10:42:00 +0800 Subject: [PATCH] Prioritize homepage summary preloading for Asia and Europe --- .env.example | 2 + data/probability_training_snapshots.jsonl | 16 ++ frontend/hooks/useDashboardStore.tsx | 246 +++++++++++++++++----- web/analysis_service.py | 44 +++- 4 files changed, 248 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index a5e39140..f4a269ca 100644 --- a/.env.example +++ b/.env.example @@ -95,6 +95,8 @@ POLYWEATHER_MONITORING_ALERT_CHAT_IDS= ######################################## NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com +# Optional: disable homepage city summary preloading. Default is enabled. +NEXT_PUBLIC_POLYWEATHER_DISABLE_EAGER_SUMMARIES=false ######################################## # 7) Optional modules diff --git a/data/probability_training_snapshots.jsonl b/data/probability_training_snapshots.jsonl index adb59a1f..b74f0633 100644 --- a/data/probability_training_snapshots.jsonl +++ b/data/probability_training_snapshots.jsonl @@ -153,3 +153,19 @@ {"city": "taipei", "timestamp": "2026-04-11T11:30:00.000Z", "date": "2026-04-11", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.3, "deb_prediction": 27.6, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 27.6}, "max_so_far": 33.0, "observation": {"current_temp": 27.0, "humidity": null, "wind_speed_kt": 4.0, "visibility_mi": null, "local_hour": 20.0}, "peak_status": "past", "prob_snapshot": [{"v": 33, "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": "taipei", "timestamp": "2026-04-11T11:30:00.000Z", "date": "2026-04-11", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.10938964843750025, "deb_prediction": 29.5, "ensemble": {"p10": 28.9, "median": 29.4, "p90": 29.8}, "multi_model": {"Open-Meteo": 27.6, "ECMWF": 31.0, "GFS": 32.4, "ICON": 27.6, "GEM": 29.7, "JMA": 28.9}, "max_so_far": 33.0, "observation": {"current_temp": 27.0, "humidity": null, "wind_speed_kt": 4.0, "visibility_mi": null, "local_hour": 20.0}, "peak_status": "past", "prob_snapshot": [{"v": 33, "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": "taipei", "timestamp": "2026-04-11T19:50:00+08:00", "date": "2026-04-11", "temp_symbol": "°C", "raw_mu": null, "raw_sigma": 0.10938964843749983, "deb_prediction": 30.3, "ensemble": {"p10": 28.8, "median": 29.3, "p90": 29.7}, "multi_model": {"Open-Meteo": 29.7, "ECMWF": 30.9, "GFS": 32.4, "ICON": 29.7, "GEM": 29.6, "JMA": 29.3}, "max_so_far": 32.1, "observation": {"current_temp": 27.6, "humidity": 73.0, "wind_speed_kt": 2.9, "visibility_mi": null, "local_hour": 20.1}, "peak_status": "past", "prob_snapshot": [{"v": 32, "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-04-12T02:00:00.000Z", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 8.5, "raw_sigma": 1.0, "deb_prediction": 8.5, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 8.5}, "max_so_far": 2.0, "observation": {"current_temp": 0.0, "humidity": null, "wind_speed_kt": 2.0, "visibility_mi": null, "local_hour": 5.166666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 8, "p": 0.35}, {"v": 9, "p": 0.35}, {"v": 10, "p": 0.139}, {"v": 7, "p": 0.139}], "shadow_prob_snapshot": [{"v": 9, "p": 0.359}, {"v": 8, "p": 0.359}, {"v": 7, "p": 0.132}, {"v": 10, "p": 0.132}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 8.5, "calibrated_sigma": 0.955} +{"city": "ankara", "timestamp": "2026-04-12T02:00:00.000Z", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 8.93, "raw_sigma": 0.3515625000000001, "deb_prediction": 9.1, "ensemble": {"p10": 8.5, "median": 9.0, "p90": 9.4}, "multi_model": {"Open-Meteo": 8.5, "ECMWF": 7.9, "GFS": 10.0, "ICON": 8.5, "GEM": 8.9, "JMA": 9.2}, "max_so_far": 2.0, "observation": {"current_temp": 0.0, "humidity": null, "wind_speed_kt": 2.0, "visibility_mi": null, "local_hour": 5.166666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 9, "p": 0.837}, {"v": 8, "p": 0.111}, {"v": 10, "p": 0.052}], "shadow_prob_snapshot": [{"v": 9, "p": 0.851}, {"v": 8, "p": 0.102}, {"v": 10, "p": 0.046}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 8.93, "calibrated_sigma": 0.3389843750000001} +{"city": "istanbul", "timestamp": "2026-04-12T04:50:00+03:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 11.7, "raw_sigma": 1.0, "deb_prediction": 11.7, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 11.7}, "max_so_far": 8.0, "observation": {"current_temp": 7.0, "humidity": 52.7, "wind_speed_kt": 1.0, "visibility_mi": null, "local_hour": 5.183333333333334}, "peak_status": "before", "prob_snapshot": [{"v": 12, "p": 0.374}, {"v": 11, "p": 0.311}, {"v": 13, "p": 0.179}, {"v": 10, "p": 0.103}], "shadow_prob_snapshot": [{"v": 12, "p": 0.387}, {"v": 11, "p": 0.316}, {"v": 13, "p": 0.174}, {"v": 10, "p": 0.095}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 11.7, "calibrated_sigma": 0.955} +{"city": "istanbul", "timestamp": "2026-04-12T04:50:00+03:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 11.52, "raw_sigma": 0.390625, "deb_prediction": 11.5, "ensemble": {"p10": 10.5, "median": 11.1, "p90": 11.5}, "multi_model": {"Open-Meteo": 11.7, "ECMWF": 11.9, "GFS": 11.1, "ICON": 11.7, "GEM": 12.8, "JMA": 10.1}, "max_so_far": 8.0, "observation": {"current_temp": 7.0, "humidity": 52.7, "wind_speed_kt": 1.0, "visibility_mi": null, "local_hour": 5.183333333333334}, "peak_status": "before", "prob_snapshot": [{"v": 12, "p": 0.52}, {"v": 11, "p": 0.48}], "shadow_prob_snapshot": [{"v": 12, "p": 0.521}, {"v": 11, "p": 0.479}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 11.52, "calibrated_sigma": 0.37609375} +{"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.2}, "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": "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.2}, "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": "taipei", "timestamp": "2026-04-12T10:00:00+08:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 30.9, "raw_sigma": 2.6999999999999993, "deb_prediction": 30.9, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 30.9}, "max_so_far": 29.2, "observation": {"current_temp": 29.2, "humidity": 64.0, "wind_speed_kt": 1.2, "visibility_mi": null, "local_hour": 10.2}, "peak_status": "before", "prob_snapshot": [{"v": 31, "p": 0.182}, {"v": 30, "p": 0.173}, {"v": 32, "p": 0.168}, {"v": 29, "p": 0.143}], "shadow_prob_snapshot": [{"v": 31, "p": 0.185}, {"v": 30, "p": 0.175}, {"v": 32, "p": 0.17}, {"v": 29, "p": 0.143}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 30.9, "calibrated_sigma": 2.6381596739291915} +{"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.2}, "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": "ankara", "timestamp": "2026-04-12T02:00:00.000Z", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 8.5, "raw_sigma": 1.0, "deb_prediction": 8.5, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 8.5}, "max_so_far": 2.0, "observation": {"current_temp": 0.0, "humidity": null, "wind_speed_kt": 2.0, "visibility_mi": null, "local_hour": 5.216666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 8, "p": 0.35}, {"v": 9, "p": 0.35}, {"v": 10, "p": 0.139}, {"v": 7, "p": 0.139}], "shadow_prob_snapshot": [{"v": 9, "p": 0.359}, {"v": 8, "p": 0.359}, {"v": 7, "p": 0.132}, {"v": 10, "p": 0.132}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 8.5, "calibrated_sigma": 0.955} +{"city": "istanbul", "timestamp": "2026-04-12T04:50:00+03:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 11.7, "raw_sigma": 1.0, "deb_prediction": 11.7, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 11.7}, "max_so_far": 8.0, "observation": {"current_temp": 7.0, "humidity": 52.7, "wind_speed_kt": 1.0, "visibility_mi": null, "local_hour": 5.233333333333333}, "peak_status": "before", "prob_snapshot": [{"v": 12, "p": 0.374}, {"v": 11, "p": 0.311}, {"v": 13, "p": 0.179}, {"v": 10, "p": 0.103}], "shadow_prob_snapshot": [{"v": 12, "p": 0.387}, {"v": 11, "p": 0.316}, {"v": 13, "p": 0.174}, {"v": 10, "p": 0.095}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 11.7, "calibrated_sigma": 0.955} +{"city": "ankara", "timestamp": "2026-04-12T02:00:00.000Z", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 8.93, "raw_sigma": 0.3515625000000001, "deb_prediction": 9.1, "ensemble": {"p10": 8.5, "median": 9.0, "p90": 9.4}, "multi_model": {"Open-Meteo": 8.5, "ECMWF": 7.9, "GFS": 10.0, "ICON": 8.5, "GEM": 8.9, "JMA": 9.2}, "max_so_far": 2.0, "observation": {"current_temp": 0.0, "humidity": null, "wind_speed_kt": 2.0, "visibility_mi": null, "local_hour": 5.25}, "peak_status": "before", "prob_snapshot": [{"v": 9, "p": 0.837}, {"v": 8, "p": 0.111}, {"v": 10, "p": 0.052}], "shadow_prob_snapshot": [{"v": 9, "p": 0.851}, {"v": 8, "p": 0.102}, {"v": 10, "p": 0.046}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 8.93, "calibrated_sigma": 0.3389843750000001} +{"city": "istanbul", "timestamp": "2026-04-12T04:50:00+03:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 11.52, "raw_sigma": 0.390625, "deb_prediction": 11.5, "ensemble": {"p10": 10.5, "median": 11.1, "p90": 11.5}, "multi_model": {"Open-Meteo": 11.7, "ECMWF": 11.9, "GFS": 11.1, "ICON": 11.7, "GEM": 12.8, "JMA": 10.1}, "max_so_far": 8.0, "observation": {"current_temp": 7.0, "humidity": 52.7, "wind_speed_kt": 1.0, "visibility_mi": null, "local_hour": 5.25}, "peak_status": "before", "prob_snapshot": [{"v": 12, "p": 0.52}, {"v": 11, "p": 0.48}], "shadow_prob_snapshot": [{"v": 12, "p": 0.521}, {"v": 11, "p": 0.479}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 11.52, "calibrated_sigma": 0.37609375} +{"city": "taipei", "timestamp": "2026-04-12T10:00:00+08:00", "date": "2026-04-12", "temp_symbol": "°C", "raw_mu": 30.9, "raw_sigma": 2.6999999999999993, "deb_prediction": 30.9, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 30.9}, "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.182}, {"v": 30, "p": 0.173}, {"v": 32, "p": 0.168}, {"v": 29, "p": 0.143}], "shadow_prob_snapshot": [{"v": 31, "p": 0.185}, {"v": 30, "p": 0.175}, {"v": 32, "p": 0.17}, {"v": 29, "p": 0.143}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 30.9, "calibrated_sigma": 2.6381596739291915} +{"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} diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index 6afa6788..6f3a6a87 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -92,8 +92,38 @@ function getMarketScanCacheKey(cityName: string, targetDate?: string | null) { const SELECTED_CITY_STORAGE_KEY = "polyWeather_selected_city_v1"; const BACKGROUND_SUMMARY_REFRESH_MS = 30_000; -const EAGER_CITY_SUMMARIES_ENABLED = - process.env.NEXT_PUBLIC_POLYWEATHER_EAGER_CITY_SUMMARIES === "true"; +const EAGER_CITY_SUMMARIES_DISABLED = + process.env.NEXT_PUBLIC_POLYWEATHER_DISABLE_EAGER_SUMMARIES === "true"; +const EAGER_SUMMARY_PRIORITY_CONCURRENCY = 3; +const EAGER_SUMMARY_BACKGROUND_CONCURRENCY = 2; +const EAGER_SUMMARY_BACKGROUND_IDLE_TIMEOUT_MS = 1200; +const EAGER_SUMMARY_PRIORITY_CITY_ORDER = [ + "hong kong", + "taipei", + "shanghai", + "beijing", + "wuhan", + "shenzhen", + "chengdu", + "chongqing", + "seoul", + "busan", + "tokyo", + "singapore", + "kuala lumpur", + "jakarta", + "tel aviv", + "jeddah", + "istanbul", + "moscow", + "helsinki", + "warsaw", + "amsterdam", + "london", + "paris", + "milan", + "madrid", +] as const; type CityDetailDepth = "panel" | "full"; function countAvailableModels( @@ -146,6 +176,57 @@ function detailSatisfiesDepth( return normalizeDetailDepth(detail) === "full"; } +function getStoredSelectedCityName(cities: CityListItem[]) { + if (typeof window === "undefined") return null; + const stored = String( + window.localStorage.getItem(SELECTED_CITY_STORAGE_KEY) || "", + ) + .trim() + .toLowerCase(); + if (!stored) return null; + if (!cities.some((city) => city.name === stored)) return null; + return stored; +} + +function getPriorityPreloadCities(cities: CityListItem[]) { + const citySet = new Set(cities.map((city) => city.name)); + return EAGER_SUMMARY_PRIORITY_CITY_ORDER.filter((cityName) => + citySet.has(cityName) + ); +} + +function scheduleWhenBrowserIdle(callback: () => void) { + if (typeof window === "undefined") { + callback(); + return () => {}; + } + + const browserWindow = window as Window & { + requestIdleCallback?: ( + cb: IdleRequestCallback, + options?: IdleRequestOptions, + ) => number; + cancelIdleCallback?: (handle: number) => void; + }; + + if (typeof browserWindow.requestIdleCallback === "function") { + const handle = browserWindow.requestIdleCallback( + () => callback(), + { timeout: EAGER_SUMMARY_BACKGROUND_IDLE_TIMEOUT_MS }, + ); + return () => { + if (typeof browserWindow.cancelIdleCallback === "function") { + browserWindow.cancelIdleCallback(handle); + } + }; + } + + const timeoutId = window.setTimeout(callback, 400); + return () => { + window.clearTimeout(timeoutId); + }; +} + export function DashboardStoreProvider({ children, }: { @@ -188,6 +269,9 @@ export function DashboardStoreProvider({ const hydratedSelectionRef = useRef(false); const hydratedProCacheRef = useRef(false); const backgroundSummaryCheckAtRef = useRef>({}); + const summaryInflightByCityRef = useRef>>( + {}, + ); const citySummariesRef = useRef>({}); const selectedCityRef = useRef(null); const selectedDetail = @@ -529,6 +613,54 @@ export function DashboardStoreProvider({ void refreshProAccess(); }, []); + const ensureCitySummary = async (cityName: string, force = false) => { + const existing = citySummariesRef.current[cityName]; + if (!force && existing) { + return existing; + } + + const inflight = summaryInflightByCityRef.current[cityName]; + if (inflight) { + const settled = await inflight; + if (!force) { + return settled; + } + } + + const request = dashboardClient + .getCitySummary(cityName, { force }) + .then((summary) => { + setCitySummariesByName((current) => { + const currentSummary = current[cityName]; + const currentRevision = getCityRevision(currentSummary); + const nextRevision = getCityRevision(summary); + if ( + currentSummary && + currentRevision && + nextRevision && + currentRevision === nextRevision + ) { + return current; + } + const next = { + ...current, + [cityName]: summary, + }; + citySummariesRef.current = next; + return next; + }); + return summary; + }) + .finally(() => { + if (summaryInflightByCityRef.current[cityName] === request) { + delete summaryInflightByCityRef.current[cityName]; + } + }); + + summaryInflightByCityRef.current[cityName] = request; + return request; + }; + useEffect(() => { if (proAccess.loading || !proAccess.authenticated || !proAccess.userId) { return; @@ -563,64 +695,72 @@ export function DashboardStoreProvider({ ]); useEffect(() => { - if (!EAGER_CITY_SUMMARIES_ENABLED) return; + if (EAGER_CITY_SUMMARIES_DISABLED) return; if (!cities.length) return; + if (loadingState.refresh) return; - const queue = cities - .map((city) => city.name) - .filter((cityName) => !citySummariesRef.current[cityName]); - if (!queue.length) return; - - let active = true; - const concurrency = 4; - let cursor = 0; - - const worker = async () => { - while (active && cursor < queue.length) { - const cityName = queue[cursor]; - cursor += 1; - if (citySummariesRef.current[cityName]) continue; - - try { - const summary = await dashboardClient.getCitySummary(cityName); - if (!active) return; - - setCitySummariesByName((current) => { - if (current[cityName]) return current; - const next = { - ...current, - [cityName]: summary, - }; - citySummariesRef.current = next; - return next; - }); - } catch {} + const priorityQueue: string[] = []; + const backgroundQueue: string[] = []; + const queued = new Set(); + const enqueue = (cityName: string | null, queue: string[]) => { + if (!cityName || queued.has(cityName) || citySummariesRef.current[cityName]) { + return; } + queued.add(cityName); + queue.push(cityName); }; - void Promise.all( - Array.from({ length: Math.min(concurrency, queue.length) }, () => - worker(), - ), - ); + enqueue(getStoredSelectedCityName(cities), priorityQueue); + getPriorityPreloadCities(cities).forEach((cityName) => { + enqueue(cityName, priorityQueue); + }); + cities.forEach((city) => { + enqueue(city.name, backgroundQueue); + }); + + if (!priorityQueue.length && !backgroundQueue.length) return; + + let active = true; + let cancelIdleSchedule = () => {}; + const runQueue = async (queue: string[], concurrency: number) => { + if (!queue.length) return; + let cursor = 0; + const worker = async () => { + while (active && cursor < queue.length) { + const cityName = queue[cursor]; + cursor += 1; + if (citySummariesRef.current[cityName]) continue; + + try { + await ensureCitySummary(cityName); + } catch {} + } + }; + + await Promise.all( + Array.from({ length: Math.min(concurrency, queue.length) }, () => + worker() + ), + ); + }; + + void (async () => { + await runQueue(priorityQueue, EAGER_SUMMARY_PRIORITY_CONCURRENCY); + if (!active) return; + cancelIdleSchedule = scheduleWhenBrowserIdle(() => { + if (!active) return; + void runQueue( + backgroundQueue, + EAGER_SUMMARY_BACKGROUND_CONCURRENCY, + ); + }); + })(); return () => { active = false; + cancelIdleSchedule(); }; - }, [cities]); - - const ensureCitySummary = async (cityName: string, force = false) => { - const existing = citySummariesRef.current[cityName]; - if (!force && existing) { - return existing; - } - const summary = await dashboardClient.getCitySummary(cityName, { force }); - setCitySummariesByName((current) => ({ - ...current, - [cityName]: summary, - })); - return summary; - }; + }, [cities, loadingState.refresh]); const selectCity = async (cityName: string) => { setSelectedCity(cityName); @@ -753,9 +893,7 @@ export function DashboardStoreProvider({ }); setSelectedForecastDate(detail.local_date); } else { - const summary = await dashboardClient.getCitySummary(selectedCity, { - force: true, - }); + const summary = await ensureCitySummary(selectedCity, true); setCitySummariesByName((current) => ({ ...current, [selectedCity]: summary, diff --git a/web/analysis_service.py b/web/analysis_service.py index d4fd0a58..8bed6aeb 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -84,6 +84,36 @@ def _analysis_ttl_for_city(city: str) -> int: return CACHE_TTL_ANKARA if city.lower() in TURKISH_MGM_CITIES else CACHE_TTL +def _analysis_cache_key(city: str, detail_mode: str = "full") -> str: + normalized_mode = "panel" if str(detail_mode or "").strip().lower() == "panel" else "full" + return f"{city}::{normalized_mode}" + + +def _get_cached_analysis( + city: str, + ttl: int, + detail_modes: tuple[str, ...] = ("panel", "full"), +) -> Optional[Dict[str, Any]]: + now_ts = _time.time() + freshest_payload: Optional[Dict[str, Any]] = None + freshest_ts = 0.0 + for detail_mode in detail_modes: + cached = _cache.get(_analysis_cache_key(city, detail_mode)) + if not cached: + continue + cached_ts = float(cached.get("t", 0)) + payload = cached.get("d") + if ( + cached_ts + and now_ts - cached_ts < ttl + and isinstance(payload, dict) + and cached_ts >= freshest_ts + ): + freshest_payload = payload + freshest_ts = cached_ts + return freshest_payload + + def _get_cached_summary(city: str, ttl: int) -> Optional[Dict[str, Any]]: now_ts = _time.time() with _SUMMARY_CACHE_LOCK: @@ -1055,9 +1085,11 @@ def _analyze( """Fetch, analyse, and return structured weather data for one city.""" # Check cache ttl = CACHE_TTL_ANKARA if city.lower() in TURKISH_MGM_CITIES else CACHE_TTL + normalized_detail_mode = "panel" if str(detail_mode or "full").strip().lower() == "panel" else "full" + cache_key = _analysis_cache_key(city, normalized_detail_mode) if not force_refresh: - cached = _cache.get(city) + cached = _cache.get(cache_key) if cached and _time.time() - cached["t"] < ttl: if include_llm_commentary: cached_payload = cached["d"] @@ -1081,7 +1113,7 @@ def _analyze( ) # ── 1. Fetch raw data ── - is_panel_mode = str(detail_mode or "full").strip().lower() == "panel" + is_panel_mode = normalized_detail_mode == "panel" raw = _weather.fetch_all_sources( city, @@ -1875,7 +1907,7 @@ def _analyze( result, ) - _cache[city] = {"t": _time.time(), "d": result} + _cache[cache_key] = {"t": _time.time(), "d": result} return result @@ -1891,9 +1923,9 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]: ttl = _analysis_ttl_for_city(city) if not force_refresh: - cached_detail = _cache.get(city) - if cached_detail and _time.time() - cached_detail["t"] < ttl: - return cached_detail["d"] + cached_detail = _get_cached_analysis(city, ttl) + if cached_detail: + return cached_detail cached_summary = _get_cached_summary(city, ttl) if cached_summary: return cached_summary