Remove Polymarket price integration from dashboard
This commit is contained in:
@@ -69,13 +69,6 @@ function formatMarketPercent(value?: number | null) {
|
|||||||
return `${(normalized * 100).toFixed(1)}%`;
|
return `${(normalized * 100).toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSignedPercent(value?: number | null) {
|
|
||||||
const numeric = Number(value);
|
|
||||||
if (!Number.isFinite(numeric)) return "--";
|
|
||||||
const sign = numeric > 0 ? "+" : "";
|
|
||||||
return `${sign}${numeric.toFixed(1)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBucketLabel(
|
function formatBucketLabel(
|
||||||
bucket?: {
|
bucket?: {
|
||||||
label?: string | null;
|
label?: string | null;
|
||||||
@@ -622,7 +615,6 @@ export function FutureForecastModal() {
|
|||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const detail = store.selectedDetail;
|
const detail = store.selectedDetail;
|
||||||
const marketScan = store.selectedMarketScan;
|
|
||||||
const dateStr = store.futureModalDate;
|
const dateStr = store.futureModalDate;
|
||||||
const isPro = store.proAccess.subscriptionActive;
|
const isPro = store.proAccess.subscriptionActive;
|
||||||
const isProLoading = store.proAccess.loading;
|
const isProLoading = store.proAccess.loading;
|
||||||
@@ -667,8 +659,7 @@ export function FutureForecastModal() {
|
|||||||
const detailDepth = detail.detail_depth || "full";
|
const detailDepth = detail.detail_depth || "full";
|
||||||
const isFullDetailReady = detailDepth === "full";
|
const isFullDetailReady = detailDepth === "full";
|
||||||
const isStructureSyncing = store.loadingState.futureDeep || !isFullDetailReady;
|
const isStructureSyncing = store.loadingState.futureDeep || !isFullDetailReady;
|
||||||
const isMarketSyncing = store.loadingState.marketScan;
|
const isAnyLayerSyncing = isStructureSyncing;
|
||||||
const isAnyLayerSyncing = isStructureSyncing || isMarketSyncing;
|
|
||||||
const view = getFutureModalView(detail, dateStr, locale);
|
const view = getFutureModalView(detail, dateStr, locale);
|
||||||
const scorePosition = `${50 + view.front.score / 2}%`;
|
const scorePosition = `${50 + view.front.score / 2}%`;
|
||||||
const barStyle = {
|
const barStyle = {
|
||||||
@@ -713,7 +704,7 @@ export function FutureForecastModal() {
|
|||||||
const boundaryRiskView = useMemo(() => {
|
const boundaryRiskView = useMemo(() => {
|
||||||
if (!showDeferredTodaySections) return null;
|
if (!showDeferredTodaySections) return null;
|
||||||
if (!isToday || !paceView) return null;
|
if (!isToday || !paceView) return null;
|
||||||
const selectedBucket = marketScan?.temperature_bucket || null;
|
const selectedBucket = topProbabilityBucket || null;
|
||||||
const bounds = parseBucketBoundaries(selectedBucket);
|
const bounds = parseBucketBoundaries(selectedBucket);
|
||||||
if (!bounds) return null;
|
if (!bounds) return null;
|
||||||
const projected =
|
const projected =
|
||||||
@@ -756,7 +747,7 @@ export function FutureForecastModal() {
|
|||||||
tone,
|
tone,
|
||||||
value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`,
|
value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`,
|
||||||
};
|
};
|
||||||
}, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, marketScan?.temperature_bucket, paceView, showDeferredTodaySections]);
|
}, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, paceView, showDeferredTodaySections, topProbabilityBucket]);
|
||||||
const peakWindowStateView = useMemo(() => {
|
const peakWindowStateView = useMemo(() => {
|
||||||
if (!showDeferredTodaySections) return null;
|
if (!showDeferredTodaySections) return null;
|
||||||
if (!isToday || !paceView) return null;
|
if (!isToday || !paceView) return null;
|
||||||
@@ -850,27 +841,7 @@ export function FutureForecastModal() {
|
|||||||
String(detail.current?.station_name || "").trim() ||
|
String(detail.current?.station_name || "").trim() ||
|
||||||
String(detail.risk?.airport || "").trim() ||
|
String(detail.risk?.airport || "").trim() ||
|
||||||
noaaStationCode;
|
noaaStationCode;
|
||||||
const topBucket = Array.isArray(marketScan?.top_buckets)
|
const hottestBucketLabel = formatBucketLabel(topProbabilityBucket);
|
||||||
? [...marketScan.top_buckets]
|
|
||||||
.map((item) => ({
|
|
||||||
...item,
|
|
||||||
probability: normalizeMarketValue(item?.probability),
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
(
|
|
||||||
item,
|
|
||||||
): item is {
|
|
||||||
label?: string | null;
|
|
||||||
bucket?: string | null;
|
|
||||||
range?: string | null;
|
|
||||||
value?: number | null;
|
|
||||||
temp?: number | null;
|
|
||||||
probability: number;
|
|
||||||
} => item.probability != null,
|
|
||||||
)
|
|
||||||
.sort((a, b) => b.probability - a.probability)[0]
|
|
||||||
: null;
|
|
||||||
const hottestBucketLabel = formatBucketLabel(topBucket);
|
|
||||||
const probabilitySummary = (() => {
|
const probabilitySummary = (() => {
|
||||||
if (!topProbabilityBucket) {
|
if (!topProbabilityBucket) {
|
||||||
return locale === "en-US"
|
return locale === "en-US"
|
||||||
@@ -904,17 +875,10 @@ export function FutureForecastModal() {
|
|||||||
})();
|
})();
|
||||||
const upperAirSignal = detail.vertical_profile_signal || {};
|
const upperAirSignal = detail.vertical_profile_signal || {};
|
||||||
const tafSignal = detail.taf?.signal || {};
|
const tafSignal = detail.taf?.signal || {};
|
||||||
const topBucketProbability = normalizeMarketValue(topBucket?.probability);
|
const upperAirCue = useMemo(() => {
|
||||||
const numericEdge = Number(marketScan?.edge_percent);
|
|
||||||
const hottestMatchesSettlement =
|
|
||||||
hottestBucketLabel !== "--" &&
|
|
||||||
formatBucketLabel(marketScan?.temperature_bucket) !== "--" &&
|
|
||||||
hottestBucketLabel === formatBucketLabel(marketScan?.temperature_bucket);
|
|
||||||
const marketAwareUpperAirCue = useMemo(() => {
|
|
||||||
if (!showDeferredTodaySections) return null;
|
if (!showDeferredTodaySections) return null;
|
||||||
if (!isToday || (!upperAirSignal.source && !tafSignal.available)) return null;
|
if (!isToday || (!upperAirSignal.source && !tafSignal.available)) return null;
|
||||||
|
|
||||||
const crowded = hottestMatchesSettlement && (topBucketProbability || 0) >= 0.3;
|
|
||||||
const setup = String(upperAirSignal.heating_setup || "neutral").toLowerCase();
|
const setup = String(upperAirSignal.heating_setup || "neutral").toLowerCase();
|
||||||
const tafSuppression = String(
|
const tafSuppression = String(
|
||||||
tafSignal.suppression_level || "low",
|
tafSignal.suppression_level || "low",
|
||||||
@@ -922,9 +886,6 @@ export function FutureForecastModal() {
|
|||||||
const tafDisruption = String(
|
const tafDisruption = String(
|
||||||
tafSignal.disruption_level || "low",
|
tafSignal.disruption_level || "low",
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
const signalLabel = String(marketScan?.signal_label || "").toUpperCase();
|
|
||||||
const edgeAbs = Number.isFinite(numericEdge) ? Math.abs(numericEdge) : 0;
|
|
||||||
const strongEdge = edgeAbs >= 8;
|
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
@@ -976,37 +937,12 @@ export function FutureForecastModal() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strongEdge && signalLabel === "BUY YES") {
|
|
||||||
score += 1;
|
|
||||||
reasons.push(
|
|
||||||
locale === "en-US"
|
|
||||||
? `market edge still leans hotter (${formatSignedPercent(numericEdge)})`
|
|
||||||
: `市场 edge 仍偏向更热一侧(${formatSignedPercent(numericEdge)})`,
|
|
||||||
);
|
|
||||||
} else if (strongEdge && signalLabel === "BUY NO") {
|
|
||||||
score -= 1;
|
|
||||||
reasons.push(
|
|
||||||
locale === "en-US"
|
|
||||||
? `market edge still leans cooler (${formatSignedPercent(numericEdge)})`
|
|
||||||
: `市场 edge 仍偏向更冷一侧(${formatSignedPercent(numericEdge)})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crowded && score > 0) {
|
|
||||||
score -= 0.5;
|
|
||||||
reasons.push(
|
|
||||||
locale === "en-US"
|
|
||||||
? "the target bucket is already getting crowded"
|
|
||||||
: "目标区间已经开始变拥挤",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score >= 1.5) {
|
if (score >= 1.5) {
|
||||||
return {
|
return {
|
||||||
summary:
|
summary:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? "The combined upper-air, TAF, and market read still leans warmer. Do not fade lower buckets too early."
|
? "The combined upper-air and TAF read still leans warmer. Do not fade lower buckets too early."
|
||||||
: "高空、TAF 和市场三层信号合并后仍偏暖侧,不宜过早做更低温区间。",
|
: "高空和 TAF 两层信号合并后仍偏暖侧,不宜过早做更低温区间。",
|
||||||
note:
|
note:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? `${reasons.slice(0, 2).join("; ")}.`
|
? `${reasons.slice(0, 2).join("; ")}.`
|
||||||
@@ -1020,8 +956,8 @@ export function FutureForecastModal() {
|
|||||||
return {
|
return {
|
||||||
summary:
|
summary:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? "The combined upper-air, TAF, and market read leans more defensive. Be more careful chasing higher buckets."
|
? "The combined upper-air and TAF read leans more defensive. Be more careful chasing higher buckets."
|
||||||
: "高空、TAF 和市场三层信号合并后更偏防守,追更高温区间要更谨慎。",
|
: "高空和 TAF 两层信号合并后更偏防守,追更高温区间要更谨慎。",
|
||||||
note:
|
note:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? `${reasons.slice(0, 2).join("; ")}.`
|
? `${reasons.slice(0, 2).join("; ")}.`
|
||||||
@@ -1034,8 +970,8 @@ export function FutureForecastModal() {
|
|||||||
return {
|
return {
|
||||||
summary:
|
summary:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? "The combined upper-air, TAF, and market read is mixed. Let surface structure and price action decide before taking a side."
|
? "The combined upper-air and TAF read is mixed. Let surface structure decide before taking a side."
|
||||||
: "高空、TAF 和市场三层信号目前偏混合,先看近地面结构和盘口变化,不急着站边。",
|
: "高空和 TAF 两层信号目前偏混合,先看近地面结构变化,不急着站边。",
|
||||||
note:
|
note:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? `${reasons.slice(0, 2).join("; ") || "No clean edge from the upper-air layer alone"}.`
|
? `${reasons.slice(0, 2).join("; ") || "No clean edge from the upper-air layer alone"}.`
|
||||||
@@ -1047,12 +983,8 @@ export function FutureForecastModal() {
|
|||||||
tafSignal.available,
|
tafSignal.available,
|
||||||
tafSignal.disruption_level,
|
tafSignal.disruption_level,
|
||||||
tafSignal.suppression_level,
|
tafSignal.suppression_level,
|
||||||
marketScan?.signal_label,
|
|
||||||
hottestMatchesSettlement,
|
|
||||||
isToday,
|
isToday,
|
||||||
locale,
|
locale,
|
||||||
numericEdge,
|
|
||||||
topBucketProbability,
|
|
||||||
upperAirSignal.heating_setup,
|
upperAirSignal.heating_setup,
|
||||||
upperAirSignal.source,
|
upperAirSignal.source,
|
||||||
showDeferredTodaySections,
|
showDeferredTodaySections,
|
||||||
@@ -1081,18 +1013,18 @@ export function FutureForecastModal() {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
const displayedUpperAirSummary = showDeferredTodaySections
|
const displayedUpperAirSummary = showDeferredTodaySections
|
||||||
? marketAwareUpperAirCue?.summary || view.front.upperAirSummary
|
? upperAirCue?.summary || view.front.upperAirSummary
|
||||||
: "";
|
: "";
|
||||||
const displayedUpperAirMetrics = showDeferredTodaySections
|
const displayedUpperAirMetrics = showDeferredTodaySections
|
||||||
? (view.front.upperAirMetrics || []).map((metric, index) =>
|
? (view.front.upperAirMetrics || []).map((metric, index) =>
|
||||||
index === 0 &&
|
index === 0 &&
|
||||||
(metric.label === "Trade cue" || metric.label === "交易动作") &&
|
(metric.label === "Trade cue" || metric.label === "交易动作") &&
|
||||||
marketAwareUpperAirCue
|
upperAirCue
|
||||||
? {
|
? {
|
||||||
...metric,
|
...metric,
|
||||||
note: marketAwareUpperAirCue.note,
|
note: upperAirCue.note,
|
||||||
tone: marketAwareUpperAirCue.tone,
|
tone: upperAirCue.tone,
|
||||||
value: marketAwareUpperAirCue.value,
|
value: upperAirCue.value,
|
||||||
}
|
}
|
||||||
: metric,
|
: metric,
|
||||||
)
|
)
|
||||||
@@ -1162,23 +1094,15 @@ export function FutureForecastModal() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "market",
|
key: "market",
|
||||||
state: isMarketSyncing ? "syncing" : "ready",
|
state: "ready",
|
||||||
label:
|
label:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? isMarketSyncing
|
? "Probability layer ready"
|
||||||
? "Syncing market ladder"
|
: "概率层已加载",
|
||||||
: "Market ladder ready"
|
|
||||||
: isMarketSyncing
|
|
||||||
? "市场挂单同步中"
|
|
||||||
: "市场挂单已加载",
|
|
||||||
note:
|
note:
|
||||||
locale === "en-US"
|
locale === "en-US"
|
||||||
? isMarketSyncing
|
? "Probability buckets are derived from the local model stack."
|
||||||
? "Polymarket buckets and edge are updating in the background."
|
: "概率桶当前由本地模型栈推导。",
|
||||||
: "Probability buckets and edge are in sync."
|
|
||||||
: isMarketSyncing
|
|
||||||
? "Polymarket 概率桶和 edge 正在后台更新。"
|
|
||||||
: "概率桶和 edge 已同步完成。",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "structure",
|
key: "structure",
|
||||||
@@ -1584,28 +1508,10 @@ export function FutureForecastModal() {
|
|||||||
{probabilitySummary}
|
{probabilitySummary}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: "relative", minHeight: "120px" }}>
|
<div style={{ position: "relative", minHeight: "120px" }}>
|
||||||
{/* Loading Overlay */}
|
|
||||||
{store.loadingState.marketScan && (
|
|
||||||
<div className="market-layer-loading-overlay">
|
|
||||||
<div
|
|
||||||
className="loading-spinner"
|
|
||||||
style={{
|
|
||||||
marginBottom: "8px",
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
borderWidth: "2px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{locale === "en-US"
|
|
||||||
? "Crunching Polymarket Edges..."
|
|
||||||
: "正在同步市场挂单..."}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ProbabilityDistribution
|
<ProbabilityDistribution
|
||||||
detail={detail}
|
detail={detail}
|
||||||
targetDate={dateStr}
|
targetDate={dateStr}
|
||||||
hideTitle
|
hideTitle
|
||||||
marketScan={marketScan}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1841,7 +1747,6 @@ export function FutureForecastModal() {
|
|||||||
detail={detail}
|
detail={detail}
|
||||||
targetDate={dateStr}
|
targetDate={dateStr}
|
||||||
hideTitle
|
hideTitle
|
||||||
marketScan={marketScan}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
@@ -1861,3 +1766,4 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
HistoryPayloadMeta,
|
HistoryPayloadMeta,
|
||||||
HistoryState,
|
HistoryState,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
MarketScan,
|
|
||||||
ProAccessState,
|
ProAccessState,
|
||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
|
|
||||||
@@ -46,12 +45,10 @@ interface DashboardStoreValue extends DashboardState {
|
|||||||
refreshAll: () => Promise<void>;
|
refreshAll: () => Promise<void>;
|
||||||
refreshProAccess: () => Promise<void>;
|
refreshProAccess: () => Promise<void>;
|
||||||
refreshSelectedCity: () => Promise<void>;
|
refreshSelectedCity: () => Promise<void>;
|
||||||
selectedMarketScan: MarketScan | null;
|
|
||||||
selectedDetail: CityDetail | null;
|
selectedDetail: CityDetail | null;
|
||||||
selectCity: (cityName: string) => Promise<void>;
|
selectCity: (cityName: string) => Promise<void>;
|
||||||
setMapInteractionActive: (active: boolean) => void;
|
setMapInteractionActive: (active: boolean) => void;
|
||||||
setForecastDate: (dateStr: string | null) => void;
|
setForecastDate: (dateStr: string | null) => void;
|
||||||
marketScanByCityName: Record<string, MarketScan>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardStoreContext = createContext<DashboardStoreValue | null>(null);
|
const DashboardStoreContext = createContext<DashboardStoreValue | null>(null);
|
||||||
@@ -94,11 +91,6 @@ function getInitialProAccessState(): ProAccessState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarketScanCacheKey(cityName: string, targetDate?: string | null) {
|
|
||||||
const normalizedDate = String(targetDate || "").trim() || "local";
|
|
||||||
return `${cityName}::${normalizedDate}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SELECTED_CITY_STORAGE_KEY = "polyWeather_selected_city_v1";
|
const SELECTED_CITY_STORAGE_KEY = "polyWeather_selected_city_v1";
|
||||||
const BACKGROUND_SUMMARY_REFRESH_MS = 30_000;
|
const BACKGROUND_SUMMARY_REFRESH_MS = 30_000;
|
||||||
type CityDetailDepth = "panel" | "market" | "nearby" | "full";
|
type CityDetailDepth = "panel" | "market" | "nearby" | "full";
|
||||||
@@ -291,9 +283,6 @@ export function DashboardStoreProvider({
|
|||||||
const [cityDetailMetaByName, setCityDetailMetaByName] = useState<
|
const [cityDetailMetaByName, setCityDetailMetaByName] = useState<
|
||||||
Record<string, { cachedAt: number; revision: string }>
|
Record<string, { cachedAt: number; revision: string }>
|
||||||
>({});
|
>({});
|
||||||
const [marketScanByCityName, setMarketScanByCityName] = useState<
|
|
||||||
Record<string, MarketScan>
|
|
||||||
>({});
|
|
||||||
const [selectedCity, setSelectedCity] = useState<string | null>(null);
|
const [selectedCity, setSelectedCity] = useState<string | null>(null);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const [selectedForecastDate, setSelectedForecastDate] = useState<
|
const [selectedForecastDate, setSelectedForecastDate] = useState<
|
||||||
@@ -325,19 +314,6 @@ export function DashboardStoreProvider({
|
|||||||
selectedCity && proAccess.subscriptionActive
|
selectedCity && proAccess.subscriptionActive
|
||||||
? cityDetailsByName[selectedCity] || null
|
? cityDetailsByName[selectedCity] || null
|
||||||
: null;
|
: null;
|
||||||
const selectedMarketDate =
|
|
||||||
futureModalDate ||
|
|
||||||
selectedForecastDate ||
|
|
||||||
selectedDetail?.local_date ||
|
|
||||||
null;
|
|
||||||
const selectedMarketScanKey = selectedCity
|
|
||||||
? getMarketScanCacheKey(selectedCity, selectedMarketDate)
|
|
||||||
: null;
|
|
||||||
const selectedMarketScan =
|
|
||||||
selectedCity && proAccess.subscriptionActive
|
|
||||||
? marketScanByCityName[selectedMarketScanKey || ""] || null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (proAccess.loading) return;
|
if (proAccess.loading) return;
|
||||||
if (!proAccess.authenticated || !proAccess.subscriptionActive) {
|
if (!proAccess.authenticated || !proAccess.subscriptionActive) {
|
||||||
@@ -402,7 +378,6 @@ export function DashboardStoreProvider({
|
|||||||
dashboardClient.clearCityDetailCache();
|
dashboardClient.clearCityDetailCache();
|
||||||
setCityDetailsByName({});
|
setCityDetailsByName({});
|
||||||
setCityDetailMetaByName({});
|
setCityDetailMetaByName({});
|
||||||
setMarketScanByCityName({});
|
|
||||||
}, [proAccess]);
|
}, [proAccess]);
|
||||||
|
|
||||||
const scheduleBackgroundDetailRefresh = (
|
const scheduleBackgroundDetailRefresh = (
|
||||||
@@ -578,32 +553,6 @@ export function DashboardStoreProvider({
|
|||||||
selectedCity,
|
selectedCity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ensureCityMarketScan = async (
|
|
||||||
cityName: string,
|
|
||||||
force = false,
|
|
||||||
marketSlug?: string | null,
|
|
||||||
targetDate?: string | null,
|
|
||||||
) => {
|
|
||||||
const cacheKey = getMarketScanCacheKey(cityName, targetDate);
|
|
||||||
const cached = marketScanByCityName[cacheKey];
|
|
||||||
if (!force && cached && !marketSlug) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestScan = await dashboardClient.getCityMarketScan(cityName, {
|
|
||||||
force,
|
|
||||||
marketSlug,
|
|
||||||
targetDate,
|
|
||||||
});
|
|
||||||
if (latestScan) {
|
|
||||||
setMarketScanByCityName((current) => ({
|
|
||||||
...current,
|
|
||||||
[cacheKey]: latestScan,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return latestScan;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadCities = async () => {
|
const loadCities = async () => {
|
||||||
setLoadingState((current) => ({ ...current, cities: true }));
|
setLoadingState((current) => ({ ...current, cities: true }));
|
||||||
try {
|
try {
|
||||||
@@ -1055,8 +1004,6 @@ export function DashboardStoreProvider({
|
|||||||
);
|
);
|
||||||
|
|
||||||
setFutureModalDate(dateStr);
|
setFutureModalDate(dateStr);
|
||||||
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
|
|
||||||
setLoadingState((current) => ({ ...current, marketScan: true }));
|
|
||||||
if (!hasMarketCachedDetail || forceRefresh) {
|
if (!hasMarketCachedDetail || forceRefresh) {
|
||||||
void ensureCityDetail(cityName, forceRefresh, "market").catch(() => {});
|
void ensureCityDetail(cityName, forceRefresh, "market").catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -1075,17 +1022,6 @@ export function DashboardStoreProvider({
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void ensureCityMarketScan(
|
|
||||||
cityName,
|
|
||||||
forceRefresh || !marketScanByCityName[cacheKey],
|
|
||||||
null,
|
|
||||||
dateStr,
|
|
||||||
)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => {
|
|
||||||
if (selectedCityRef.current !== cityName) return;
|
|
||||||
setLoadingState((current) => ({ ...current, marketScan: false }));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
openHistory,
|
openHistory,
|
||||||
openTodayModal: async (forceRefresh?: boolean) => {
|
openTodayModal: async (forceRefresh?: boolean) => {
|
||||||
@@ -1129,7 +1065,6 @@ export function DashboardStoreProvider({
|
|||||||
setLoadingState((current) => ({
|
setLoadingState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
futureDeep: needsDetailRefresh,
|
futureDeep: needsDetailRefresh,
|
||||||
marketScan: true,
|
|
||||||
}));
|
}));
|
||||||
if (!hasMarketCachedDetail || forceRefresh) {
|
if (!hasMarketCachedDetail || forceRefresh) {
|
||||||
void ensureCityDetail(
|
void ensureCityDetail(
|
||||||
@@ -1138,26 +1073,6 @@ export function DashboardStoreProvider({
|
|||||||
"market",
|
"market",
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
}
|
}
|
||||||
const initialTargetDate =
|
|
||||||
cachedDetail?.local_date || selectedForecastDate || null;
|
|
||||||
const initialMarketKey = getMarketScanCacheKey(
|
|
||||||
cityName,
|
|
||||||
initialTargetDate,
|
|
||||||
);
|
|
||||||
void ensureCityMarketScan(
|
|
||||||
cityName,
|
|
||||||
forceRefresh || !marketScanByCityName[initialMarketKey],
|
|
||||||
null,
|
|
||||||
initialTargetDate,
|
|
||||||
)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => {
|
|
||||||
if (selectedCityRef.current !== cityName) return;
|
|
||||||
setLoadingState((current) => ({
|
|
||||||
...current,
|
|
||||||
marketScan: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
void ensureCityDetail(
|
void ensureCityDetail(
|
||||||
cityName,
|
cityName,
|
||||||
needsDetailRefresh,
|
needsDetailRefresh,
|
||||||
@@ -1189,7 +1104,6 @@ export function DashboardStoreProvider({
|
|||||||
refreshAll,
|
refreshAll,
|
||||||
refreshProAccess,
|
refreshProAccess,
|
||||||
refreshSelectedCity,
|
refreshSelectedCity,
|
||||||
selectedMarketScan,
|
|
||||||
selectedCity,
|
selectedCity,
|
||||||
selectedDetail,
|
selectedDetail,
|
||||||
selectedForecastDate,
|
selectedForecastDate,
|
||||||
@@ -1197,7 +1111,6 @@ export function DashboardStoreProvider({
|
|||||||
setMapInteractionActive: setIsMapInteracting,
|
setMapInteractionActive: setIsMapInteracting,
|
||||||
setForecastDate: (dateStr: string | null) =>
|
setForecastDate: (dateStr: string | null) =>
|
||||||
setSelectedForecastDate(dateStr),
|
setSelectedForecastDate(dateStr),
|
||||||
marketScanByCityName,
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
cities,
|
cities,
|
||||||
@@ -1208,8 +1121,6 @@ export function DashboardStoreProvider({
|
|||||||
isPanelOpen,
|
isPanelOpen,
|
||||||
loadingState,
|
loadingState,
|
||||||
proAccess,
|
proAccess,
|
||||||
marketScanByCityName,
|
|
||||||
selectedMarketScan,
|
|
||||||
selectedCity,
|
selectedCity,
|
||||||
selectedDetail,
|
selectedDetail,
|
||||||
selectedForecastDate,
|
selectedForecastDate,
|
||||||
@@ -1259,3 +1170,4 @@ export function useHistoryData(name?: string | null) {
|
|||||||
meta: key ? store.historyState.metaByCity[key] || null : null,
|
meta: key ? store.historyState.metaByCity[key] || null : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import {
|
import {
|
||||||
CityDetail,
|
CityDetail,
|
||||||
CityListItem,
|
CityListItem,
|
||||||
MarketScan,
|
|
||||||
CitySummary,
|
CitySummary,
|
||||||
HistoryPayload,
|
HistoryPayload,
|
||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
@@ -13,7 +12,6 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
|
|||||||
const pendingCityDetailRequests = new Map<string, Promise<CityDetail>>();
|
const pendingCityDetailRequests = new Map<string, Promise<CityDetail>>();
|
||||||
const pendingHistoryRequests = new Map<string, Promise<HistoryPayload>>();
|
const pendingHistoryRequests = new Map<string, Promise<HistoryPayload>>();
|
||||||
const pendingCitySummaryRequests = new Map<string, Promise<CitySummary>>();
|
const pendingCitySummaryRequests = new Map<string, Promise<CitySummary>>();
|
||||||
const pendingMarketScanRequests = new Map<string, Promise<MarketScan | null>>();
|
|
||||||
|
|
||||||
type CityCacheMeta = {
|
type CityCacheMeta = {
|
||||||
cachedAt: number;
|
cachedAt: number;
|
||||||
@@ -195,64 +193,6 @@ export const dashboardClient = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getCityMarketScan(
|
|
||||||
cityName: string,
|
|
||||||
options?: {
|
|
||||||
force?: boolean;
|
|
||||||
marketSlug?: string | null;
|
|
||||||
targetDate?: string | null;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const force = options?.force ?? false;
|
|
||||||
const marketSlug = options?.marketSlug || null;
|
|
||||||
const targetDate = options?.targetDate || null;
|
|
||||||
if (!force) {
|
|
||||||
const requestKey = `${cityName}::cached::${marketSlug || "-"}::${
|
|
||||||
targetDate || "-"
|
|
||||||
}`;
|
|
||||||
const existing = pendingMarketScanRequests.get(requestKey);
|
|
||||||
if (existing) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
force_refresh: "false",
|
|
||||||
});
|
|
||||||
if (marketSlug) {
|
|
||||||
params.set("market_slug", marketSlug);
|
|
||||||
}
|
|
||||||
if (targetDate) {
|
|
||||||
params.set("target_date", targetDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = fetchJson<{ market_scan?: MarketScan }>(
|
|
||||||
`/api/city/${normalizeCityName(cityName)}/market-scan?${params.toString()}`,
|
|
||||||
)
|
|
||||||
.then((data) => data.market_scan || null)
|
|
||||||
.finally(() => {
|
|
||||||
pendingMarketScanRequests.delete(requestKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
pendingMarketScanRequests.set(requestKey, request);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
force_refresh: "true",
|
|
||||||
_ts: String(Date.now()),
|
|
||||||
});
|
|
||||||
if (marketSlug) {
|
|
||||||
params.set("market_slug", marketSlug);
|
|
||||||
}
|
|
||||||
if (targetDate) {
|
|
||||||
params.set("target_date", targetDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchJson<{ market_scan?: MarketScan }>(
|
|
||||||
`/api/city/${normalizeCityName(cityName)}/market-scan?${params.toString()}`,
|
|
||||||
).then((data) => data.market_scan || null);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getHistory(cityName: string, options?: { includeRecords?: boolean }) {
|
async getHistory(cityName: string, options?: { includeRecords?: boolean }) {
|
||||||
const includeRecords = options?.includeRecords === true;
|
const includeRecords = options?.includeRecords === true;
|
||||||
const requestKey = `${normalizeCityName(cityName)}::${
|
const requestKey = `${normalizeCityName(cityName)}::${
|
||||||
|
|||||||
@@ -556,3 +556,4 @@ export interface DashboardState {
|
|||||||
historyState: HistoryState;
|
historyState: HistoryState;
|
||||||
proAccess: ProAccessState;
|
proAccess: ProAccessState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-15
@@ -600,21 +600,7 @@ async def auth_me(request: Request):
|
|||||||
user_id,
|
user_id,
|
||||||
respect_requirement=False,
|
respect_requirement=False,
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
not latest_subscription
|
|
||||||
and getattr(PAYMENT_CHECKOUT, "enabled", False)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
PAYMENT_CHECKOUT.reconcile_latest_intent(user_id)
|
|
||||||
latest_subscription = SUPABASE_ENTITLEMENT.get_latest_active_subscription(
|
|
||||||
user_id,
|
|
||||||
respect_requirement=False,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
latest_subscription = SUPABASE_ENTITLEMENT.get_latest_active_subscription(
|
|
||||||
user_id,
|
|
||||||
respect_requirement=False,
|
|
||||||
)
|
|
||||||
latest_known_subscription = latest_subscription
|
latest_known_subscription = latest_subscription
|
||||||
if not latest_known_subscription:
|
if not latest_known_subscription:
|
||||||
latest_known_subscription = (
|
latest_known_subscription = (
|
||||||
@@ -1119,3 +1105,4 @@ async def city_market_scan(
|
|||||||
market_slug,
|
market_slug,
|
||||||
target_date,
|
target_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user