Hydrate homepage market data
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user