Use direct scan rows for model summary
This commit is contained in:
@@ -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<number | null>(null);
|
||||
const [feedbackDraft, setFeedbackDraft] = useState<FeedbackDraft | null>(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" ? (
|
||||
<TrainingDashboard isEn={isEn} />
|
||||
) : activeNavKey === "modelSummary" ? (
|
||||
<ModelSummaryDashboard rows={rows} isEn={isEn} generatedText={generatedText} />
|
||||
<ModelSummaryDashboard
|
||||
rows={modelSummaryRows}
|
||||
isEn={isEn}
|
||||
generatedText={modelSummaryGeneratedText}
|
||||
/>
|
||||
) : activeNavKey === "guide" ? (
|
||||
<UsageGuideDashboard isEn={isEn} />
|
||||
) : (
|
||||
|
||||
@@ -250,9 +250,12 @@ export function runTests() {
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes("<ModelSummaryDashboard") &&
|
||||
dashboardSource.includes("rows={rows}") &&
|
||||
dashboardSource.includes("generatedText={generatedText}"),
|
||||
"terminal model summary view must use existing scan rows instead of fetching city detail",
|
||||
dashboardSource.includes("rows={modelSummaryRows}") &&
|
||||
dashboardSource.includes("cacheScope: \"model-summary\"") &&
|
||||
dashboardSource.includes("modelSummary: true") &&
|
||||
dashboardSource.includes("hasTerminalForecastRows(rows)") &&
|
||||
dashboardSource.includes("generatedText={modelSummaryGeneratedText}"),
|
||||
"terminal model summary view must use direct scan terminal rows without city fallback rows",
|
||||
);
|
||||
assert(
|
||||
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
|
||||
|
||||
@@ -178,7 +178,7 @@ export function runTests() {
|
||||
assert(
|
||||
scanQuerySource.includes("MAX_STALE_SCAN_CACHE_MS") &&
|
||||
scanQuerySource.includes("allowStale") &&
|
||||
scanQuerySource.includes("setCachedRows(readScanCache(tradingRegion || \"\", { allowStale: true }))"),
|
||||
scanQuerySource.includes("setCachedRows(readScanCache(tradingRegion || \"\", cacheScope, { allowStale: true }))"),
|
||||
"terminal data hook must render stale scan rows immediately while revalidating the first-screen API",
|
||||
);
|
||||
assert(
|
||||
|
||||
@@ -26,6 +26,7 @@ const SCAN_TERMINAL_PAYLOAD_VERSION = "runway-slim-v1";
|
||||
|
||||
type TerminalQueryOptions = {
|
||||
forceRefresh?: boolean;
|
||||
modelSummary?: boolean;
|
||||
signal?: AbortSignal;
|
||||
sinceSnapshotId?: string | null;
|
||||
timezoneOffsetSeconds?: number | null;
|
||||
@@ -123,6 +124,7 @@ async function readJsonOrThrow<T>(path: string, init?: RequestInit): Promise<T>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<ScanTerminalResponse | null>(() => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user