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()) {
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user