Add market detail depth caching for dashboard data
This commit is contained in:
@@ -177,3 +177,9 @@
|
|||||||
{"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": "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.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}
|
{"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}
|
||||||
|
{"city": "busan", "timestamp": "2026-04-14T03:00:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 19.3, "raw_sigma": 1.8499999999999996, "deb_prediction": 17.1, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 15.8, "ECMWF": 17.1, "GFS": 16.3, "ICON": 18.4, "GEM": 19.5, "JMA": 15.8}, "max_so_far": 19.0, "observation": {"current_temp": 19.0, "humidity": null, "wind_speed_kt": 8.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 19, "p": 0.321}, {"v": 20, "p": 0.303}, {"v": 21, "p": 0.215}, {"v": 22, "p": 0.115}], "shadow_prob_snapshot": [{"v": 19, "p": 0.254}, {"v": 20, "p": 0.247}, {"v": 21, "p": 0.204}, {"v": 22, "p": 0.144}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 19.3, "calibrated_sigma": 2.4974999999999996}
|
||||||
|
{"city": "busan", "timestamp": "2026-04-14T03:00:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 19.3, "raw_sigma": 0.9765625, "deb_prediction": 17.1, "ensemble": {"p10": 15.0, "median": 16.0, "p90": 17.5}, "multi_model": {"Open-Meteo": 15.8, "ECMWF": 17.1, "GFS": 16.3, "ICON": 18.4, "GEM": 19.5, "JMA": 15.8}, "max_so_far": 19.0, "observation": {"current_temp": 19.0, "humidity": null, "wind_speed_kt": 8.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 19, "p": 0.48}, {"v": 20, "p": 0.396}, {"v": 21, "p": 0.125}], "shadow_prob_snapshot": [{"v": 19, "p": 0.4}, {"v": 20, "p": 0.359}, {"v": 21, "p": 0.186}, {"v": 22, "p": 0.055}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 19.3, "calibrated_sigma": 1.318359375}
|
||||||
|
{"city": "seoul", "timestamp": "2026-04-14T03:30:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 23.3, "raw_sigma": 3.8000000000000007, "deb_prediction": 18.7, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 16.3, "ECMWF": 22.3, "GFS": 14.7, "ICON": 21.0, "GEM": 21.8, "JMA": 16.3}, "max_so_far": 23.0, "observation": {"current_temp": 23.0, "humidity": null, "wind_speed_kt": 5.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 23, "p": 0.184}, {"v": 24, "p": 0.181}, {"v": 25, "p": 0.167}, {"v": 26, "p": 0.143}], "shadow_prob_snapshot": [{"v": 23, "p": 0.221}, {"v": 24, "p": 0.216}, {"v": 25, "p": 0.189}, {"v": 26, "p": 0.148}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 23.3, "calibrated_sigma": 3.0}
|
||||||
|
{"city": "seoul", "timestamp": "2026-04-14T03:30:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 23.3, "raw_sigma": 0.6666666666666673, "deb_prediction": 18.7, "ensemble": {"p10": 17.7, "median": 18.2, "p90": 19.3}, "multi_model": {"Open-Meteo": 16.3, "ECMWF": 22.3, "GFS": 14.7, "ICON": 21.0, "GEM": 21.8, "JMA": 16.3}, "max_so_far": 23.0, "observation": {"current_temp": 23.0, "humidity": null, "wind_speed_kt": 5.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 23, "p": 0.569}, {"v": 24, "p": 0.391}, {"v": 25, "p": 0.04}], "shadow_prob_snapshot": [{"v": 23, "p": 0.498}, {"v": 24, "p": 0.398}, {"v": 25, "p": 0.104}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 23.3, "calibrated_sigma": 0.9000000000000009}
|
||||||
|
{"city": "tokyo", "timestamp": "2026-04-14T03:30:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 22.7, "raw_sigma": 1.3499999999999996, "deb_prediction": 22.3, "ensemble": {"p10": null, "median": null, "p90": null}, "multi_model": {"Open-Meteo": 20.6, "ECMWF": 22.9, "GFS": 22.7, "ICON": 21.7, "GEM": 23.3, "JMA": 20.6}, "max_so_far": 20.0, "observation": {"current_temp": 20.0, "humidity": null, "wind_speed_kt": 7.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 23, "p": 0.285}, {"v": 22, "p": 0.257}, {"v": 24, "p": 0.188}, {"v": 21, "p": 0.137}], "shadow_prob_snapshot": [{"v": 23, "p": 0.294}, {"v": 22, "p": 0.263}, {"v": 24, "p": 0.188}, {"v": 21, "p": 0.134}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 22.7, "calibrated_sigma": 1.3014441113240975}
|
||||||
|
{"city": "tokyo", "timestamp": "2026-04-14T03:30:00.000Z", "date": "2026-04-14", "temp_symbol": "°C", "raw_mu": 23.089999999999996, "raw_sigma": 1.2890624999999998, "deb_prediction": 22.3, "ensemble": {"p10": 21.8, "median": 24.0, "p90": 25.0}, "multi_model": {"Open-Meteo": 20.6, "ECMWF": 22.9, "GFS": 22.7, "ICON": 21.7, "GEM": 23.3, "JMA": 20.6}, "max_so_far": 20.0, "observation": {"current_temp": 20.0, "humidity": null, "wind_speed_kt": 7.0, "visibility_mi": null, "local_hour": 12.966666666666667}, "peak_status": "before", "prob_snapshot": [{"v": 23, "p": 0.303}, {"v": 24, "p": 0.24}, {"v": 22, "p": 0.216}, {"v": 25, "p": 0.107}], "shadow_prob_snapshot": [{"v": 23, "p": 0.313}, {"v": 24, "p": 0.244}, {"v": 22, "p": 0.218}, {"v": 25, "p": 0.103}], "probability_engine": "legacy", "probability_mode": "emos_shadow", "calibration_version": "emos-20260402162744", "calibration_source": "artifacts\\probability_calibration\\default.json", "calibrated_mu": 23.089999999999996, "calibrated_sigma": 1.2428312132581776}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const EAGER_SUMMARY_PRIORITY_CITY_ORDER = [
|
|||||||
"milan",
|
"milan",
|
||||||
"madrid",
|
"madrid",
|
||||||
] as const;
|
] as const;
|
||||||
type CityDetailDepth = "panel" | "nearby" | "full";
|
type CityDetailDepth = "panel" | "market" | "nearby" | "full";
|
||||||
|
|
||||||
function countAvailableModels(
|
function countAvailableModels(
|
||||||
detail?: CityDetail | null,
|
detail?: CityDetail | null,
|
||||||
@@ -172,7 +172,16 @@ function hasSparseDetailCoverage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasMarketDetailCoverage(
|
||||||
|
detail?: CityDetail | null,
|
||||||
|
targetDate?: string | null,
|
||||||
|
): boolean {
|
||||||
|
if (!detail) return false;
|
||||||
|
return countAvailableModels(detail, targetDate) > 1;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeDetailDepth(detail?: CityDetail | null): CityDetailDepth {
|
function normalizeDetailDepth(detail?: CityDetail | null): CityDetailDepth {
|
||||||
|
if (detail?.detail_depth === "market") return "market";
|
||||||
if (detail?.detail_depth === "nearby") return "nearby";
|
if (detail?.detail_depth === "nearby") return "nearby";
|
||||||
if (detail?.detail_depth === "panel") return "panel";
|
if (detail?.detail_depth === "panel") return "panel";
|
||||||
return "full";
|
return "full";
|
||||||
@@ -181,9 +190,18 @@ function normalizeDetailDepth(detail?: CityDetail | null): CityDetailDepth {
|
|||||||
function detailSatisfiesDepth(
|
function detailSatisfiesDepth(
|
||||||
detail: CityDetail | null | undefined,
|
detail: CityDetail | null | undefined,
|
||||||
depth: CityDetailDepth,
|
depth: CityDetailDepth,
|
||||||
|
targetDate?: string | null,
|
||||||
) {
|
) {
|
||||||
if (!detail) return false;
|
if (!detail) return false;
|
||||||
if (depth === "panel") return true;
|
if (depth === "panel") return true;
|
||||||
|
if (depth === "market") {
|
||||||
|
const normalized = normalizeDetailDepth(detail);
|
||||||
|
return (
|
||||||
|
normalized === "market" ||
|
||||||
|
normalized === "full" ||
|
||||||
|
hasMarketDetailCoverage(detail, targetDate)
|
||||||
|
);
|
||||||
|
}
|
||||||
if (depth === "nearby") {
|
if (depth === "nearby") {
|
||||||
const normalized = normalizeDetailDepth(detail);
|
const normalized = normalizeDetailDepth(detail);
|
||||||
return normalized === "nearby" || normalized === "full";
|
return normalized === "nearby" || normalized === "full";
|
||||||
@@ -192,7 +210,67 @@ function detailSatisfiesDepth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldCheckSparseCoverageForDepth(depth: CityDetailDepth) {
|
function shouldCheckSparseCoverageForDepth(depth: CityDetailDepth) {
|
||||||
return depth === "panel" || depth === "full";
|
return depth === "panel" || depth === "market" || depth === "full";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulModelMap(
|
||||||
|
value: Record<string, number | null> | undefined,
|
||||||
|
): value is Record<string, number | null> {
|
||||||
|
return Boolean(
|
||||||
|
value &&
|
||||||
|
Object.values(value).some((entry) => Number.isFinite(Number(entry))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulDailyModelMap(
|
||||||
|
value: CityDetail["multi_model_daily"] | undefined,
|
||||||
|
) {
|
||||||
|
return Boolean(
|
||||||
|
value &&
|
||||||
|
Object.values(value).some((day) =>
|
||||||
|
hasMeaningfulModelMap(day?.models || undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCityDetail(
|
||||||
|
current: CityDetail | undefined,
|
||||||
|
incoming: CityDetail,
|
||||||
|
): CityDetail {
|
||||||
|
if (!current) return incoming;
|
||||||
|
if (incoming.detail_depth !== "market") return incoming;
|
||||||
|
|
||||||
|
const mergedDepth =
|
||||||
|
current.detail_depth === "full" || current.detail_depth === "nearby"
|
||||||
|
? current.detail_depth
|
||||||
|
: incoming.detail_depth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
...incoming,
|
||||||
|
detail_depth: mergedDepth,
|
||||||
|
current: incoming.current || current.current,
|
||||||
|
airport_current: incoming.airport_current || current.airport_current,
|
||||||
|
deb: incoming.deb || current.deb,
|
||||||
|
probabilities: incoming.probabilities || current.probabilities,
|
||||||
|
trend: incoming.trend || current.trend,
|
||||||
|
multi_model: hasMeaningfulModelMap(incoming.multi_model)
|
||||||
|
? incoming.multi_model
|
||||||
|
: current.multi_model,
|
||||||
|
multi_model_daily: hasMeaningfulDailyModelMap(incoming.multi_model_daily)
|
||||||
|
? {
|
||||||
|
...(current.multi_model_daily || {}),
|
||||||
|
...(incoming.multi_model_daily || {}),
|
||||||
|
}
|
||||||
|
: current.multi_model_daily,
|
||||||
|
forecast: current.forecast || incoming.forecast,
|
||||||
|
official_nearby: current.official_nearby || incoming.official_nearby,
|
||||||
|
mgm_nearby: current.mgm_nearby || incoming.mgm_nearby,
|
||||||
|
network_lead_signal:
|
||||||
|
current.network_lead_signal || incoming.network_lead_signal,
|
||||||
|
airport_vs_network_delta:
|
||||||
|
current.airport_vs_network_delta ?? incoming.airport_vs_network_delta,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredSelectedCityName(cities: CityListItem[]) {
|
function getStoredSelectedCityName(cities: CityListItem[]) {
|
||||||
@@ -420,7 +498,7 @@ export function DashboardStoreProvider({
|
|||||||
|
|
||||||
setCityDetailsByName((current) => ({
|
setCityDetailsByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[cityName]: detail,
|
[cityName]: mergeCityDetail(current[cityName], detail),
|
||||||
}));
|
}));
|
||||||
setCitySummariesByName((current) => ({
|
setCitySummariesByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -444,10 +522,18 @@ export function DashboardStoreProvider({
|
|||||||
) => {
|
) => {
|
||||||
const cached = cityDetailsByName[cityName];
|
const cached = cityDetailsByName[cityName];
|
||||||
const cachedMeta = cityDetailMetaByName[cityName];
|
const cachedMeta = cityDetailMetaByName[cityName];
|
||||||
const hasRequestedDepth = detailSatisfiesDepth(cached, depth);
|
const marketTargetDate =
|
||||||
|
depth === "market" ? selectedForecastDate || cached?.local_date : null;
|
||||||
|
const hasRequestedDepth = detailSatisfiesDepth(
|
||||||
|
cached,
|
||||||
|
depth,
|
||||||
|
marketTargetDate,
|
||||||
|
);
|
||||||
const cachedIsSparse =
|
const cachedIsSparse =
|
||||||
shouldCheckSparseCoverageForDepth(depth) &&
|
shouldCheckSparseCoverageForDepth(depth) &&
|
||||||
hasSparseDetailCoverage(cached, cached?.local_date);
|
(depth === "market"
|
||||||
|
? hasSparseModelCoverage(cached, marketTargetDate)
|
||||||
|
: hasSparseDetailCoverage(cached, cached?.local_date));
|
||||||
if (
|
if (
|
||||||
!force &&
|
!force &&
|
||||||
cached &&
|
cached &&
|
||||||
@@ -472,7 +558,7 @@ export function DashboardStoreProvider({
|
|||||||
const detail = latestDetail;
|
const detail = latestDetail;
|
||||||
setCityDetailsByName((current) => ({
|
setCityDetailsByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[cityName]: detail,
|
[cityName]: mergeCityDetail(current[cityName], detail),
|
||||||
}));
|
}));
|
||||||
setCitySummariesByName((current) => ({
|
setCitySummariesByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -508,7 +594,7 @@ export function DashboardStoreProvider({
|
|||||||
const detail = latestDetail;
|
const detail = latestDetail;
|
||||||
setCityDetailsByName((current) => ({
|
setCityDetailsByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[cityName]: detail,
|
[cityName]: mergeCityDetail(current[cityName], detail),
|
||||||
}));
|
}));
|
||||||
setCitySummariesByName((current) => ({
|
setCitySummariesByName((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -858,6 +944,7 @@ export function DashboardStoreProvider({
|
|||||||
if (selectedCityRef.current !== cityName) return;
|
if (selectedCityRef.current !== cityName) return;
|
||||||
setSelectedForecastDate(detail.local_date);
|
setSelectedForecastDate(detail.local_date);
|
||||||
if (access.authenticated && access.subscriptionActive) {
|
if (access.authenticated && access.subscriptionActive) {
|
||||||
|
void ensureCityDetail(cityName, false, "market").catch(() => {});
|
||||||
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
||||||
void ensureCityMarketScan(
|
void ensureCityMarketScan(
|
||||||
cityName,
|
cityName,
|
||||||
@@ -1101,10 +1188,18 @@ export function DashboardStoreProvider({
|
|||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
||||||
|
const hasMarketCachedDetail = detailSatisfiesDepth(
|
||||||
|
cachedDetail,
|
||||||
|
"market",
|
||||||
|
dateStr,
|
||||||
|
);
|
||||||
|
|
||||||
setFutureModalDate(dateStr);
|
setFutureModalDate(dateStr);
|
||||||
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
||||||
setLoadingState((current) => ({ ...current, marketScan: true }));
|
setLoadingState((current) => ({ ...current, marketScan: true }));
|
||||||
|
if (!hasMarketCachedDetail || forceRefresh) {
|
||||||
|
void ensureCityDetail(cityName, forceRefresh, "market").catch(() => {});
|
||||||
|
}
|
||||||
if (!hasFullCachedDetail || forceRefresh) {
|
if (!hasFullCachedDetail || forceRefresh) {
|
||||||
setLoadingState((current) => ({
|
setLoadingState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -1154,6 +1249,11 @@ export function DashboardStoreProvider({
|
|||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
||||||
|
const hasMarketCachedDetail = detailSatisfiesDepth(
|
||||||
|
cachedDetail,
|
||||||
|
"market",
|
||||||
|
cachedDetail?.local_date,
|
||||||
|
);
|
||||||
const targetDate =
|
const targetDate =
|
||||||
cachedDetail?.local_date || selectedForecastDate || null;
|
cachedDetail?.local_date || selectedForecastDate || null;
|
||||||
if (targetDate) {
|
if (targetDate) {
|
||||||
@@ -1171,6 +1271,13 @@ export function DashboardStoreProvider({
|
|||||||
futureDeep: needsDetailRefresh,
|
futureDeep: needsDetailRefresh,
|
||||||
marketScan: true,
|
marketScan: true,
|
||||||
}));
|
}));
|
||||||
|
if (!hasMarketCachedDetail || forceRefresh) {
|
||||||
|
void ensureCityDetail(
|
||||||
|
cityName,
|
||||||
|
Boolean(forceRefresh),
|
||||||
|
"market",
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
const initialTargetDate =
|
const initialTargetDate =
|
||||||
cachedDetail?.local_date || selectedForecastDate || null;
|
cachedDetail?.local_date || selectedForecastDate || null;
|
||||||
const initialMarketKey = getMarketScanCacheKey(
|
const initialMarketKey = getMarketScanCacheKey(
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ function normalizeCityName(cityName: string) {
|
|||||||
return encodeURIComponent(String(cityName).replace(/\s/g, "-"));
|
return encodeURIComponent(String(cityName).replace(/\s/g, "-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDetailDepth(depth?: "panel" | "nearby" | "full") {
|
function normalizeDetailDepth(depth?: "panel" | "market" | "nearby" | "full") {
|
||||||
if (depth === "full") return "full";
|
if (depth === "full") return "full";
|
||||||
if (depth === "nearby") return "nearby";
|
if (depth === "nearby") return "nearby";
|
||||||
|
if (depth === "market") return "market";
|
||||||
return "panel";
|
return "panel";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ export const dashboardClient = {
|
|||||||
|
|
||||||
async getCityDetail(
|
async getCityDetail(
|
||||||
cityName: string,
|
cityName: string,
|
||||||
options?: { force?: boolean; depth?: "panel" | "nearby" | "full" },
|
options?: { force?: boolean; depth?: "panel" | "market" | "nearby" | "full" },
|
||||||
) {
|
) {
|
||||||
const force = options?.force ?? false;
|
const force = options?.force ?? false;
|
||||||
const depth = normalizeDetailDepth(options?.depth);
|
const depth = normalizeDetailDepth(options?.depth);
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ export interface AiAnalysisStructured {
|
|||||||
export interface CityDetail {
|
export interface CityDetail {
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
detail_depth?: "panel" | "nearby" | "full";
|
detail_depth?: "panel" | "market" | "nearby" | "full";
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
temp_symbol: string;
|
temp_symbol: string;
|
||||||
|
|||||||
@@ -439,6 +439,8 @@ async def city_detail(
|
|||||||
normalized_depth = str(depth or "panel").strip().lower()
|
normalized_depth = str(depth or "panel").strip().lower()
|
||||||
if normalized_depth == "full":
|
if normalized_depth == "full":
|
||||||
detail_mode = "full"
|
detail_mode = "full"
|
||||||
|
elif normalized_depth == "market":
|
||||||
|
detail_mode = "market"
|
||||||
elif normalized_depth == "nearby":
|
elif normalized_depth == "nearby":
|
||||||
detail_mode = "nearby"
|
detail_mode = "nearby"
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user