Use direct scan rows for model summary

This commit is contained in:
2569718930@qq.com
2026-06-30 18:23:05 +08:00
parent 35a9d8e943
commit a2f169f008
5 changed files with 67 additions and 22 deletions
@@ -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()) { function isFutureAccessExpiry(value: string | null | undefined, now = Date.now()) {
if (!value) return true; if (!value) return true;
const ts = Date.parse(value); const ts = Date.parse(value);
@@ -751,6 +759,28 @@ function PolyWeatherTerminal({
const [onlineCount, setOnlineCount] = useState<number | null>(null); const [onlineCount, setOnlineCount] = useState<number | null>(null);
const [feedbackDraft, setFeedbackDraft] = useState<FeedbackDraft | null>(null); const [feedbackDraft, setFeedbackDraft] = useState<FeedbackDraft | null>(null);
const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0); 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 trialExpiryMs = Date.parse(String(trialSubscriptionExpiresAt || ""));
const trialHoursLeft = Number.isFinite(trialExpiryMs) const trialHoursLeft = Number.isFinite(trialExpiryMs)
? Math.max(0, Math.ceil((trialExpiryMs - Date.now()) / 3_600_000)) ? Math.max(0, Math.ceil((trialExpiryMs - Date.now()) / 3_600_000))
@@ -1126,7 +1156,11 @@ function PolyWeatherTerminal({
{activeNavKey === "training" ? ( {activeNavKey === "training" ? (
<TrainingDashboard isEn={isEn} /> <TrainingDashboard isEn={isEn} />
) : activeNavKey === "modelSummary" ? ( ) : activeNavKey === "modelSummary" ? (
<ModelSummaryDashboard rows={rows} isEn={isEn} generatedText={generatedText} /> <ModelSummaryDashboard
rows={modelSummaryRows}
isEn={isEn}
generatedText={modelSummaryGeneratedText}
/>
) : activeNavKey === "guide" ? ( ) : activeNavKey === "guide" ? (
<UsageGuideDashboard isEn={isEn} /> <UsageGuideDashboard isEn={isEn} />
) : ( ) : (
@@ -250,9 +250,12 @@ export function runTests() {
); );
assert( assert(
dashboardSource.includes("<ModelSummaryDashboard") && dashboardSource.includes("<ModelSummaryDashboard") &&
dashboardSource.includes("rows={rows}") && dashboardSource.includes("rows={modelSummaryRows}") &&
dashboardSource.includes("generatedText={generatedText}"), dashboardSource.includes("cacheScope: \"model-summary\"") &&
"terminal model summary view must use existing scan rows instead of fetching city detail", 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( assert(
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") && modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
@@ -178,7 +178,7 @@ export function runTests() {
assert( assert(
scanQuerySource.includes("MAX_STALE_SCAN_CACHE_MS") && scanQuerySource.includes("MAX_STALE_SCAN_CACHE_MS") &&
scanQuerySource.includes("allowStale") && 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", "terminal data hook must render stale scan rows immediately while revalidating the first-screen API",
); );
assert( assert(
@@ -26,6 +26,7 @@ const SCAN_TERMINAL_PAYLOAD_VERSION = "runway-slim-v1";
type TerminalQueryOptions = { type TerminalQueryOptions = {
forceRefresh?: boolean; forceRefresh?: boolean;
modelSummary?: boolean;
signal?: AbortSignal; signal?: AbortSignal;
sinceSnapshotId?: string | null; sinceSnapshotId?: string | null;
timezoneOffsetSeconds?: number | null; timezoneOffsetSeconds?: number | null;
@@ -123,6 +124,7 @@ async function readJsonOrThrow<T>(path: string, init?: RequestInit): Promise<T>
async function getTerminal({ async function getTerminal({
forceRefresh = false, forceRefresh = false,
modelSummary = false,
signal, signal,
sinceSnapshotId, sinceSnapshotId,
timezoneOffsetSeconds, timezoneOffsetSeconds,
@@ -139,14 +141,14 @@ async function getTerminal({
limit: "180", limit: "180",
force_refresh: String(forceRefresh), force_refresh: String(forceRefresh),
}); });
if (tradingRegion && tradingRegion !== "all") { if (!modelSummary && tradingRegion && tradingRegion !== "all") {
params.set("trading_region", tradingRegion); params.set("trading_region", tradingRegion);
} }
if (Number.isFinite(timezoneOffsetSeconds)) { if (!modelSummary && Number.isFinite(timezoneOffsetSeconds)) {
params.set("timezone_offset_seconds", String(Math.trunc(Number(timezoneOffsetSeconds)))); params.set("timezone_offset_seconds", String(Math.trunc(Number(timezoneOffsetSeconds))));
} }
const trimmedSnapshotId = String(sinceSnapshotId || "").trim(); const trimmedSnapshotId = String(sinceSnapshotId || "").trim();
if (!forceRefresh && trimmedSnapshotId) { if (!modelSummary && !forceRefresh && trimmedSnapshotId) {
params.set("diff", "true"); params.set("diff", "true");
params.set("since_snapshot_id", trimmedSnapshotId); params.set("since_snapshot_id", trimmedSnapshotId);
} }
@@ -18,7 +18,7 @@ import type {
ScanTerminalResponse, ScanTerminalResponse,
} from "@/lib/dashboard-types"; } 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 SCAN_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.scanRows;
const FOREGROUND_SCAN_REFRESH_AFTER_SUCCESS_MS = 60_000; const FOREGROUND_SCAN_REFRESH_AFTER_SUCCESS_MS = 60_000;
const MAX_STALE_SCAN_CACHE_MS = 6 * 60 * 60 * 1000; const MAX_STALE_SCAN_CACHE_MS = 6 * 60 * 60 * 1000;
@@ -32,16 +32,17 @@ const DERIVED_SCAN_PATCH_NUMBER_FIELDS = [
"deb_prediction", "deb_prediction",
] as const; ] as const;
function scanCacheKey(tradingRegion: string): string { function scanCacheKey(tradingRegion: string, cacheScope: string): string {
return `${SCAN_CACHE_PREFIX}:${tradingRegion || "all"}`; return `${SCAN_CACHE_PREFIX}:${cacheScope || "terminal"}:${tradingRegion || "all"}`;
} }
function readScanCache( function readScanCache(
tradingRegion: string, tradingRegion: string,
cacheScope: string,
options?: { allowStale?: boolean }, options?: { allowStale?: boolean },
): ScanTerminalResponse | null { ): ScanTerminalResponse | null {
try { try {
const raw = localStorage.getItem(scanCacheKey(tradingRegion)); const raw = localStorage.getItem(scanCacheKey(tradingRegion, cacheScope));
if (!raw) return null; if (!raw) return null;
const cached = JSON.parse(raw); const cached = JSON.parse(raw);
const age = Date.now() - Number(cached.ts || 0); const age = Date.now() - Number(cached.ts || 0);
@@ -53,8 +54,8 @@ function readScanCache(
return null; return null;
} }
function writeScanCache(data: ScanTerminalResponse, tradingRegion: string) { function writeScanCache(data: ScanTerminalResponse, tradingRegion: string, cacheScope: string) {
try { localStorage.setItem(scanCacheKey(tradingRegion), JSON.stringify({ ts: Date.now(), data })); } catch { /* ignore */ } try { localStorage.setItem(scanCacheKey(tradingRegion, cacheScope), JSON.stringify({ ts: Date.now(), data })); } catch { /* ignore */ }
} }
function normalizeCityKey(city: string | null | undefined) { function normalizeCityKey(city: string | null | undefined) {
@@ -225,13 +226,17 @@ function mergeScanTerminalIncrementalResponse(
} }
export function useScanTerminalQuery({ export function useScanTerminalQuery({
cacheScope = "terminal",
isPro, isPro,
modelSummary = false,
proAccessLoading, proAccessLoading,
terminalActivationRefreshKey = 0, terminalActivationRefreshKey = 0,
timezoneOffsetSeconds, timezoneOffsetSeconds,
tradingRegion, tradingRegion,
}: { }: {
cacheScope?: string;
isPro: boolean; isPro: boolean;
modelSummary?: boolean;
proAccessLoading: boolean; proAccessLoading: boolean;
terminalActivationRefreshKey?: number; terminalActivationRefreshKey?: number;
timezoneOffsetSeconds?: number | null; timezoneOffsetSeconds?: number | null;
@@ -252,7 +257,7 @@ export function useScanTerminalQuery({
const patchVersion = useSsePatchVersion(); const patchVersion = useSsePatchVersion();
const [cachedRows, setCachedRows] = useState<ScanTerminalResponse | null>(() => { const [cachedRows, setCachedRows] = useState<ScanTerminalResponse | null>(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
return readScanCache(tradingRegion || "", { allowStale: true }); return readScanCache(tradingRegion || "", cacheScope, { allowStale: true });
} }
return null; return null;
}); });
@@ -264,8 +269,8 @@ export function useScanTerminalQuery({
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
setCachedRows(readScanCache(tradingRegion || "", { allowStale: true })); setCachedRows(readScanCache(tradingRegion || "", cacheScope, { allowStale: true }));
}, [tradingRegion]); }, [cacheScope, tradingRegion]);
const fetchScanTerminal = useCallback( const fetchScanTerminal = useCallback(
async ({ async ({
@@ -287,6 +292,7 @@ export function useScanTerminalQuery({
const previous = latestScanDataRef.current; const previous = latestScanDataRef.current;
const next = await scanTerminalClient.getTerminal({ const next = await scanTerminalClient.getTerminal({
forceRefresh, forceRefresh,
modelSummary,
signal, signal,
sinceSnapshotId: !forceRefresh ? previous?.snapshot_id : null, sinceSnapshotId: !forceRefresh ? previous?.snapshot_id : null,
timezoneOffsetSeconds, timezoneOffsetSeconds,
@@ -297,12 +303,12 @@ export function useScanTerminalQuery({
showLoading, showLoading,
onSuccess: (data) => { onSuccess: (data) => {
lastScanSuccessAtRef.current = Date.now(); lastScanSuccessAtRef.current = Date.now();
writeScanCache(data, tradingRegion || ""); writeScanCache(data, tradingRegion || "", cacheScope);
setCachedRows(data); setCachedRows(data);
}, },
}); });
}, },
[isPro, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion], [cacheScope, isPro, modelSummary, proAccessLoading, run, timezoneOffsetSeconds, tradingRegion],
); );
useEffect(() => { useEffect(() => {
@@ -392,14 +398,14 @@ export function useScanTerminalQuery({
if (!neighbors.length) return; if (!neighbors.length) return;
const preloadOne = (region: string) => { 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 idleFn = (window as any).requestIdleCallback || ((cb: () => void) => setTimeout(cb, 5000));
const handle = idleFn(() => { const handle = idleFn(() => {
void scanTerminalClient.getTerminal({ void scanTerminalClient.getTerminal({
forceRefresh: false, forceRefresh: false,
signal: new AbortController().signal, signal: new AbortController().signal,
tradingRegion: region, tradingRegion: region,
}).then((data) => { writeScanCache(data, region); }); }).then((data) => { writeScanCache(data, region, cacheScope); });
}); });
return handle; return handle;
}; };
@@ -409,7 +415,7 @@ export function useScanTerminalQuery({
const cancelFn = (window as any).cancelIdleCallback || clearTimeout; const cancelFn = (window as any).cancelIdleCallback || clearTimeout;
handles.forEach((h) => { try { cancelFn(h); } catch { /* */ } }); handles.forEach((h) => { try { cancelFn(h); } catch { /* */ } });
}; };
}, [tradingRegion, isPro]); }, [cacheScope, tradingRegion, isPro]);
return { return {
refreshScanTerminalManually, refreshScanTerminalManually,