Hydrate homepage market data

This commit is contained in:
2569718930@qq.com
2026-04-23 00:02:25 +08:00
parent 5dcf98b810
commit c3ca20c849
4 changed files with 88 additions and 11 deletions
@@ -36,9 +36,7 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`; const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`;
try { try {
const auth = await buildBackendRequestHeaders(req, { const auth = await buildBackendRequestHeaders(req);
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", cache: "no-store",
+1 -3
View File
@@ -127,9 +127,7 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`; const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`;
try { try {
const auth = await buildBackendRequestHeaders(req, { const auth = await buildBackendRequestHeaders(req);
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", cache: "no-store",
@@ -197,6 +197,12 @@ function buildSparklinePoints(values: number[] | undefined) {
.join(" "); .join(" ");
} }
function readNumericField(source: unknown, key: string) {
if (!source || typeof source !== "object") return undefined;
const value = (source as Record<string, unknown>)[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) { function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
const store = useDashboardStore(); const store = useDashboardStore();
const { locale } = useI18n(); const { locale } = useI18n();
@@ -237,13 +243,35 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
const modelLabels = getModelLabels(detail); const modelLabels = getModelLabels(detail);
const marketScan = detail?.market_scan; const marketScan = detail?.market_scan;
const marketBucket = marketScan?.temperature_bucket || marketScan?.top_buckets?.[0] || null; const marketBucket = marketScan?.temperature_bucket || marketScan?.top_buckets?.[0] || null;
const yesPrice =
marketScan?.yes_buy ??
readNumericField(marketBucket, "yes_buy") ??
marketScan?.yes_token?.buy_price ??
marketScan?.yes_token?.midpoint ??
marketScan?.price_analysis?.yes?.ask;
const noPrice =
marketScan?.no_buy ??
readNumericField(marketBucket, "no_buy") ??
marketScan?.no_token?.buy_price ??
marketScan?.no_token?.midpoint ??
marketScan?.price_analysis?.no?.ask;
const marketEdge =
marketScan?.edge_percent ??
marketScan?.price_analysis?.yes?.edge_percent ??
marketScan?.price_analysis?.yes?.edge ??
marketScan?.price_analysis?.no?.edge_percent ??
marketScan?.price_analysis?.no?.edge;
const marketLabel = marketBucket const marketLabel = marketBucket
? getProbabilityLabel(marketBucket, symbol) ? getProbabilityLabel(marketBucket, symbol)
: debPrediction != null : debPrediction != null
? `> ${formatTemperature(debPrediction, symbol)}` ? `> ${formatTemperature(debPrediction, symbol)}`
: "--"; : "--";
const marketProbability = const marketProbability =
marketScan?.market_price ?? marketScan?.yes_buy ?? marketScan?.model_probability; marketScan?.market_price ??
readNumericField(marketBucket, "market_price") ??
readNumericField(marketBucket, "yes_buy") ??
yesPrice ??
marketScan?.model_probability;
const marketModelProbability = marketScan?.model_probability ?? marketBucket?.probability; const marketModelProbability = marketScan?.model_probability ?? marketBucket?.probability;
const sparklinePoints = buildSparklinePoints(marketScan?.sparkline); const sparklinePoints = buildSparklinePoints(marketScan?.sparkline);
const isLoading = store.loadingState.cityDetail && store.selectedCity === city.name; const isLoading = store.loadingState.cityDetail && store.selectedCity === city.name;
@@ -258,7 +286,7 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
: getRiskCopy(riskLevel, locale); : getRiskCopy(riskLevel, locale);
const keySignals = [ const keySignals = [
{ {
active: Number(marketScan?.edge_percent) > 0, active: Number(marketEdge) > 0,
label: label:
locale === "en-US" locale === "en-US"
? "DEB > Market implied" ? "DEB > Market implied"
@@ -411,13 +439,13 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
<span>{marketScan?.selected_date || detail?.local_date || ""}</span> <span>{marketScan?.selected_date || detail?.local_date || ""}</span>
</div> </div>
<div className="home-market-prices"> <div className="home-market-prices">
<span className="yes">YES {formatCents(marketScan?.yes_buy)}</span> <span className="yes">YES {formatCents(yesPrice)}</span>
<span className="no">NO {formatCents(marketScan?.no_buy)}</span> <span className="no">NO {formatCents(noPrice)}</span>
</div> </div>
</div> </div>
<div className="home-market-metrics"> <div className="home-market-metrics">
<span> <span>
Edge <strong>{formatEdge(marketScan?.edge_percent)}</strong> Edge <strong>{formatEdge(marketEdge)}</strong>
</span> </span>
<span> <span>
Implied <strong>{formatProbability(marketProbability)}</strong> Implied <strong>{formatProbability(marketProbability)}</strong>
@@ -531,6 +559,7 @@ function DashboardScreen() {
const store = useDashboardStore(); const store = useDashboardStore();
const { t } = useI18n(); const { t } = useI18n();
const didAutoFocusRef = useRef(false); const didAutoFocusRef = useRef(false);
const preloadedOpportunityRef = useRef<Set<string>>(new Set());
const activeSummary = store.selectedCity const activeSummary = store.selectedCity
? store.citySummariesByName[store.selectedCity] || null ? store.citySummariesByName[store.selectedCity] || null
: null; : null;
@@ -603,6 +632,18 @@ function DashboardScreen() {
void store.focusCity(topOpportunity); void store.focusCity(topOpportunity);
}, [homepageSnapshots, showHomepageChrome, store]); }, [homepageSnapshots, showHomepageChrome, store]);
useEffect(() => {
if (!showHomepageChrome) return;
const targets = homepageSnapshots.slice(0, 4).map((snapshot) => snapshot.city.name);
targets.forEach((cityName) => {
if (preloadedOpportunityRef.current.has(cityName)) return;
preloadedOpportunityRef.current.add(cityName);
void store.ensureCityDetail(cityName, false, "panel").catch(() => {
preloadedOpportunityRef.current.delete(cityName);
});
});
}, [homepageSnapshots, showHomepageChrome, store]);
return ( return (
<div <div
className={clsx( className={clsx(
+40
View File
@@ -555,6 +555,31 @@ export function DashboardStoreProvider({
return detail; return detail;
}; };
const ensureCityMarketScan = async (cityName: string, force = false) => {
const cached = cityDetailsByName[cityName];
try {
const payload = await dashboardClient.getCityMarketScan(cityName, {
force,
targetDate: cached?.local_date || selectedForecastDate || null,
});
if (!payload.market_scan) return null;
setCityDetailsByName((current) => {
const detail = current[cityName];
if (!detail) return current;
return {
...current,
[cityName]: {
...detail,
market_scan: payload.market_scan || undefined,
},
};
});
return payload.market_scan;
} catch {
return null;
}
};
useEffect(() => { useEffect(() => {
if (proAccess.loading) return; if (proAccess.loading) return;
if (!selectedCity) return; if (!selectedCity) return;
@@ -826,6 +851,12 @@ export function DashboardStoreProvider({
if (selectedCityRef.current !== cityName) return; if (selectedCityRef.current !== cityName) return;
if (detail.status === "fulfilled") { if (detail.status === "fulfilled") {
setSelectedForecastDate(detail.value.local_date); setSelectedForecastDate(detail.value.local_date);
if (
proAccessRef.current.subscriptionActive &&
!detail.value.market_scan
) {
void ensureCityMarketScan(cityName, false);
}
} }
}) })
.finally(() => { .finally(() => {
@@ -834,6 +865,15 @@ export function DashboardStoreProvider({
}); });
}; };
useEffect(() => {
if (!selectedCity) return;
if (!proAccess.subscriptionActive) return;
const detail = cityDetailsByName[selectedCity];
if (!detail) return;
if (detail.market_scan) return;
void ensureCityMarketScan(selectedCity, false);
}, [cityDetailsByName, proAccess.subscriptionActive, selectedCity]);
const clearCityFocus = () => { const clearCityFocus = () => {
selectedCityRef.current = null; selectedCityRef.current = null;
setSelectedCity(null); setSelectedCity(null);