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()) {
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,