diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index e58e12e0..fd5a74f4 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -142,6 +142,14 @@ function createLocalAccess(): ProAccessState { }; } +function hasTerminalForecastRows(rows: ScanOpportunityRow[]) { + return rows.some((row) => { + if (row.deb_prediction != null) return true; + const sources = row.model_cluster_sources; + return Boolean(sources && Object.keys(sources).length > 0); + }); +} + function isFutureAccessExpiry(value: string | null | undefined, now = Date.now()) { if (!value) return true; const ts = Date.parse(value); @@ -751,6 +759,28 @@ function PolyWeatherTerminal({ const [onlineCount, setOnlineCount] = useState(null); const [feedbackDraft, setFeedbackDraft] = useState(null); const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0); + const { terminalData: modelSummaryTerminalData } = useScanTerminalQuery({ + cacheScope: "model-summary", + isPro: activeNavKey === "modelSummary", + modelSummary: true, + proAccessLoading: false, + terminalActivationRefreshKey, + timezoneOffsetSeconds: null, + tradingRegion: "all", + }); + const modelSummaryRows = useMemo( + () => + sortRowsByUserTime( + modelSummaryTerminalData?.rows?.length + ? modelSummaryTerminalData.rows + : hasTerminalForecastRows(rows) + ? rows + : [], + ), + [modelSummaryTerminalData?.rows, rows], + ); + const modelSummaryGeneratedText = + useRelativeTime(modelSummaryTerminalData?.generated_at ?? null) || generatedText; const trialExpiryMs = Date.parse(String(trialSubscriptionExpiresAt || "")); const trialHoursLeft = Number.isFinite(trialExpiryMs) ? Math.max(0, Math.ceil((trialExpiryMs - Date.now()) / 3_600_000)) @@ -1126,7 +1156,11 @@ function PolyWeatherTerminal({ {activeNavKey === "training" ? ( ) : activeNavKey === "modelSummary" ? ( - + ) : activeNavKey === "guide" ? ( ) : ( diff --git a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts index ae353704..0da66584 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts @@ -250,9 +250,12 @@ export function runTests() { ); assert( dashboardSource.includes("(path: string, init?: RequestInit): Promise async function getTerminal({ forceRefresh = false, + modelSummary = false, signal, sinceSnapshotId, timezoneOffsetSeconds, @@ -139,14 +141,14 @@ async function getTerminal({ limit: "180", force_refresh: String(forceRefresh), }); - if (tradingRegion && tradingRegion !== "all") { + if (!modelSummary && tradingRegion && tradingRegion !== "all") { params.set("trading_region", tradingRegion); } - if (Number.isFinite(timezoneOffsetSeconds)) { + if (!modelSummary && Number.isFinite(timezoneOffsetSeconds)) { params.set("timezone_offset_seconds", String(Math.trunc(Number(timezoneOffsetSeconds)))); } const trimmedSnapshotId = String(sinceSnapshotId || "").trim(); - if (!forceRefresh && trimmedSnapshotId) { + if (!modelSummary && !forceRefresh && trimmedSnapshotId) { params.set("diff", "true"); params.set("since_snapshot_id", trimmedSnapshotId); } diff --git a/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts index c9582720..4ab1f6aa 100644 --- a/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts +++ b/frontend/components/dashboard/scan-terminal/use-scan-terminal-query.ts @@ -18,7 +18,7 @@ import type { ScanTerminalResponse, } from "@/lib/dashboard-types"; -const SCAN_CACHE_PREFIX = "polyweather_scan_v2"; +const SCAN_CACHE_PREFIX = "polyweather_scan_v3"; const SCAN_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.scanRows; const FOREGROUND_SCAN_REFRESH_AFTER_SUCCESS_MS = 60_000; const MAX_STALE_SCAN_CACHE_MS = 6 * 60 * 60 * 1000; @@ -32,16 +32,17 @@ const DERIVED_SCAN_PATCH_NUMBER_FIELDS = [ "deb_prediction", ] as const; -function scanCacheKey(tradingRegion: string): string { - return `${SCAN_CACHE_PREFIX}:${tradingRegion || "all"}`; +function scanCacheKey(tradingRegion: string, cacheScope: string): string { + return `${SCAN_CACHE_PREFIX}:${cacheScope || "terminal"}:${tradingRegion || "all"}`; } function readScanCache( tradingRegion: string, + cacheScope: string, options?: { allowStale?: boolean }, ): ScanTerminalResponse | null { try { - const raw = localStorage.getItem(scanCacheKey(tradingRegion)); + const raw = localStorage.getItem(scanCacheKey(tradingRegion, cacheScope)); if (!raw) return null; const cached = JSON.parse(raw); const age = Date.now() - Number(cached.ts || 0); @@ -53,8 +54,8 @@ function readScanCache( return null; } -function writeScanCache(data: ScanTerminalResponse, tradingRegion: string) { - try { localStorage.setItem(scanCacheKey(tradingRegion), JSON.stringify({ ts: Date.now(), data })); } catch { /* ignore */ } +function writeScanCache(data: ScanTerminalResponse, tradingRegion: string, cacheScope: string) { + try { localStorage.setItem(scanCacheKey(tradingRegion, cacheScope), JSON.stringify({ ts: Date.now(), data })); } catch { /* ignore */ } } function normalizeCityKey(city: string | null | undefined) { @@ -225,13 +226,17 @@ function mergeScanTerminalIncrementalResponse( } export function useScanTerminalQuery({ + cacheScope = "terminal", isPro, + modelSummary = false, proAccessLoading, terminalActivationRefreshKey = 0, timezoneOffsetSeconds, tradingRegion, }: { + cacheScope?: string; isPro: boolean; + modelSummary?: boolean; proAccessLoading: boolean; terminalActivationRefreshKey?: number; timezoneOffsetSeconds?: number | null; @@ -252,7 +257,7 @@ export function useScanTerminalQuery({ const patchVersion = useSsePatchVersion(); const [cachedRows, setCachedRows] = useState(() => { if (typeof window !== "undefined") { - return readScanCache(tradingRegion || "", { allowStale: true }); + return readScanCache(tradingRegion || "", cacheScope, { allowStale: true }); } return null; }); @@ -264,8 +269,8 @@ export function useScanTerminalQuery({ useEffect(() => { if (typeof window === "undefined") return; - setCachedRows(readScanCache(tradingRegion || "", { allowStale: true })); - }, [tradingRegion]); + setCachedRows(readScanCache(tradingRegion || "", cacheScope, { allowStale: true })); + }, [cacheScope, tradingRegion]); const fetchScanTerminal = useCallback( async ({ @@ -287,6 +292,7 @@ export function useScanTerminalQuery({ const previous = latestScanDataRef.current; const next = await scanTerminalClient.getTerminal({ forceRefresh, + modelSummary, signal, sinceSnapshotId: !forceRefresh ? previous?.snapshot_id : null, timezoneOffsetSeconds, @@ -297,12 +303,12 @@ export function useScanTerminalQuery({ showLoading, onSuccess: (data) => { lastScanSuccessAtRef.current = Date.now(); - writeScanCache(data, tradingRegion || ""); + writeScanCache(data, tradingRegion || "", cacheScope); setCachedRows(data); }, }); }, - [isPro, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion], + [cacheScope, isPro, modelSummary, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion], ); useEffect(() => { @@ -392,14 +398,14 @@ export function useScanTerminalQuery({ if (!neighbors.length) return; const preloadOne = (region: string) => { - if (readScanCache(region)) return; // already cached + if (readScanCache(region, cacheScope)) return; // already cached const idleFn = (window as any).requestIdleCallback || ((cb: () => void) => setTimeout(cb, 5000)); const handle = idleFn(() => { void scanTerminalClient.getTerminal({ forceRefresh: false, signal: new AbortController().signal, tradingRegion: region, - }).then((data) => { writeScanCache(data, region); }); + }).then((data) => { writeScanCache(data, region, cacheScope); }); }); return handle; }; @@ -409,7 +415,7 @@ export function useScanTerminalQuery({ const cancelFn = (window as any).cancelIdleCallback || clearTimeout; handles.forEach((h) => { try { cancelFn(h); } catch { /* */ } }); }; - }, [tradingRegion, isPro]); + }, [cacheScope, tradingRegion, isPro]); return { refreshScanTerminalManually,