Optimize terminal startup and deployment smoke checks
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||
import { REGIONS, getCityRegion } from "./continent-grouping";
|
||||
@@ -126,6 +126,7 @@ export function CitySelectorDropdown({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const deferredSearchQuery = useDeferredValue(searchQuery);
|
||||
const [activeTab, setActiveTab] = useState<string>("all");
|
||||
const [viewportNudgeY, setViewportNudgeY] = useState(0);
|
||||
|
||||
@@ -170,7 +171,7 @@ export function CitySelectorDropdown({
|
||||
|
||||
// Filter rows
|
||||
const filteredRows = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
const q = deferredSearchQuery.toLowerCase().trim();
|
||||
return rows.filter((row) => {
|
||||
// 1. Region filter
|
||||
if (activeTab !== "all") {
|
||||
@@ -195,7 +196,7 @@ export function CitySelectorDropdown({
|
||||
.map((s) => s!.toLowerCase());
|
||||
return haystack.some((s) => s.includes(q));
|
||||
});
|
||||
}, [rows, searchQuery, activeTab]);
|
||||
}, [rows, deferredSearchQuery, activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
let frame = 0;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||
import { useLatestPatch, useSseResyncVersion } from "@/hooks/use-sse-patches";
|
||||
import { Panel } from "@/components/dashboard/scan-terminal/Panel";
|
||||
import { ModelCurvesSummary } from "@/components/dashboard/scan-terminal/ModelCurvesSummary";
|
||||
import { TemperatureChartCanvas } from "@/components/dashboard/scan-terminal/TemperatureChartCanvas";
|
||||
import { TemperatureChartCanvasFallback } from "@/components/dashboard/scan-terminal/TemperatureChartCanvasFallback";
|
||||
import { TemperatureRunwayDetails } from "@/components/dashboard/scan-terminal/TemperatureRunwayDetails";
|
||||
import { TemperatureStatsBars } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
|
||||
import { rowName } from "@/components/dashboard/scan-terminal/utils";
|
||||
@@ -56,6 +57,17 @@ const PEAK_GLOW_BADGE_CLASS = {
|
||||
|
||||
const PROBABILITY_REFRESH_AFTER_PATCH_MS = 60_000;
|
||||
|
||||
const TemperatureChartCanvas = dynamic(
|
||||
() =>
|
||||
import("@/components/dashboard/scan-terminal/TemperatureChartCanvas").then(
|
||||
(mod) => mod.TemperatureChartCanvas,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <TemperatureChartCanvasFallback />,
|
||||
},
|
||||
);
|
||||
|
||||
function peakGlowLabel(state: keyof typeof PEAK_GLOW_PANEL_CLASS, isEn: boolean) {
|
||||
if (state === "watch") return isEn ? "Watch" : "关注";
|
||||
if (state === "near_peak") return isEn ? "Near peak" : "接近峰值";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export function TemperatureChartCanvasFallback({ compact }: { compact?: boolean }) {
|
||||
const horizontalLines = compact ? 5 : 7;
|
||||
const verticalLines = compact ? 5 : 8;
|
||||
const minChartHeight = compact === false ? 220 : 120;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex-1 overflow-hidden rounded-sm border border-slate-100 bg-white"
|
||||
style={{ minHeight: minChartHeight }}
|
||||
>
|
||||
<div className="absolute inset-x-3 bottom-7 top-4 rounded-sm border border-slate-100">
|
||||
{Array.from({ length: horizontalLines }).map((_, index) => (
|
||||
<span
|
||||
key={`h-${index}`}
|
||||
className="absolute left-0 right-0 border-t border-dashed border-sky-100"
|
||||
style={{ top: `${(index / Math.max(1, horizontalLines - 1)) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: verticalLines }).map((_, index) => (
|
||||
<span
|
||||
key={`v-${index}`}
|
||||
className="absolute bottom-0 top-0 border-l border-dashed border-sky-100"
|
||||
style={{ left: `${(index / Math.max(1, verticalLines - 1)) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 grid place-items-center">
|
||||
<div className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-500 shadow-sm">
|
||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-blue-200 border-t-blue-500" />
|
||||
加载图表
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,10 @@ export function runTests() {
|
||||
path.join(projectRoot, "components", "dashboard", "scan-terminal", "LiveTemperatureThresholdChart.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const terminalPageSource = fs.readFileSync(
|
||||
path.join(projectRoot, "app", "terminal", "page.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const chartCanvasSource = fs.readFileSync(
|
||||
path.join(projectRoot, "components", "dashboard", "scan-terminal", "TemperatureChartCanvas.tsx"),
|
||||
"utf8",
|
||||
@@ -28,6 +32,10 @@ export function runTests() {
|
||||
path.join(projectRoot, "components", "dashboard", "scan-terminal", "CitySelectorDropdown.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const scanQuerySource = fs.readFileSync(
|
||||
path.join(projectRoot, "components", "dashboard", "scan-terminal", "use-scan-terminal-query.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert(
|
||||
dashboardSource.includes("MAX_TERMINAL_CHARTS = 9"),
|
||||
@@ -75,6 +83,26 @@ export function runTests() {
|
||||
citySelectorSource.includes("getBoundingClientRect()"),
|
||||
"city selector dropdown must nudge itself inside the viewport when opened from top-row chart cards",
|
||||
);
|
||||
assert(
|
||||
terminalPageSource.includes("dynamic(") &&
|
||||
terminalPageSource.includes('import("@/components/dashboard/ScanTerminalDashboard")') &&
|
||||
terminalPageSource.includes("DashboardShellSkeleton") &&
|
||||
!terminalPageSource.includes('import { ScanTerminalDashboard } from "@/components/dashboard/ScanTerminalDashboard";'),
|
||||
"terminal route must dynamically load the heavy dashboard behind the shell skeleton",
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes('from "next/dynamic"') &&
|
||||
dashboardSource.includes('import("@/components/dashboard/scan-terminal/TrainingDashboard")') &&
|
||||
!dashboardSource.includes('import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";'),
|
||||
"terminal dashboard must lazy-load the training analytics tab so Recharts stays out of the default terminal path",
|
||||
);
|
||||
assert(
|
||||
chartSource.includes('from "next/dynamic"') &&
|
||||
chartSource.includes("TemperatureChartCanvasFallback") &&
|
||||
chartSource.includes("import(\"@/components/dashboard/scan-terminal/TemperatureChartCanvas\")") &&
|
||||
!chartSource.includes('import { TemperatureChartCanvas } from "@/components/dashboard/scan-terminal/TemperatureChartCanvas";'),
|
||||
"terminal temperature charts must lazy-load the Recharts canvas behind a lightweight fallback",
|
||||
);
|
||||
assert(
|
||||
chartSource.includes("setLiveTemp(null);") &&
|
||||
chartSource.includes("lastAppliedPatchRevisionRef.current = 0;"),
|
||||
@@ -109,6 +137,25 @@ export function runTests() {
|
||||
dashboardSource.includes("[rows, deferredSearchQuery]"),
|
||||
"terminal search must defer expensive row filtering so typing stays responsive",
|
||||
);
|
||||
assert(
|
||||
scanQuerySource.includes("MAX_STALE_SCAN_CACHE_MS") &&
|
||||
scanQuerySource.includes("allowStale") &&
|
||||
scanQuerySource.includes("setCachedRows(readScanCache(tradingRegion || \"\", { allowStale: true }))"),
|
||||
"terminal data hook must render stale scan rows immediately while revalidating the first-screen API",
|
||||
);
|
||||
assert(
|
||||
citySelectorSource.includes("useDeferredValue") &&
|
||||
citySelectorSource.includes("deferredSearchQuery") &&
|
||||
citySelectorSource.includes("[rows, deferredSearchQuery, activeTab]"),
|
||||
"city selector search must defer expensive dropdown filtering so top-row selection stays responsive",
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes("accessDecisionPending") &&
|
||||
dashboardSource.includes("shouldShowPaywall") &&
|
||||
dashboardSource.indexOf("if (accessDecisionPending)") <
|
||||
dashboardSource.indexOf("if (shouldShowPaywall)"),
|
||||
"terminal must keep showing verification while access is undecided instead of flashing the paywall",
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes('trackAppEvent("enter_terminal"') &&
|
||||
dashboardSource.includes('entry: "terminal"'),
|
||||
|
||||
@@ -17,17 +17,23 @@ import type { ScanTerminalResponse } from "@/lib/dashboard-types";
|
||||
|
||||
const SCAN_CACHE_PREFIX = "polyweather_scan_v2";
|
||||
const SCAN_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.scanRows;
|
||||
const MAX_STALE_SCAN_CACHE_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
function scanCacheKey(tradingRegion: string): string {
|
||||
return `${SCAN_CACHE_PREFIX}:${tradingRegion || "all"}`;
|
||||
}
|
||||
|
||||
function readScanCache(tradingRegion: string): ScanTerminalResponse | null {
|
||||
function readScanCache(
|
||||
tradingRegion: string,
|
||||
options?: { allowStale?: boolean },
|
||||
): ScanTerminalResponse | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(scanCacheKey(tradingRegion));
|
||||
if (!raw) return null;
|
||||
const cached = JSON.parse(raw);
|
||||
if (cached.ts && Date.now() - cached.ts < SCAN_CACHE_TTL_MS && cached.data?.rows) {
|
||||
const age = Date.now() - Number(cached.ts || 0);
|
||||
const maxAge = options?.allowStale ? MAX_STALE_SCAN_CACHE_MS : SCAN_CACHE_TTL_MS;
|
||||
if (cached.ts && age >= 0 && age < maxAge && cached.data?.rows) {
|
||||
return cached.data;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
@@ -99,10 +105,17 @@ export function useScanTerminalQuery({
|
||||
const lastForcedScanRefreshAtRef = useRef(0);
|
||||
const patchVersion = useSsePatchVersion();
|
||||
const [cachedRows, setCachedRows] = useState<ScanTerminalResponse | null>(() => {
|
||||
if (typeof window !== "undefined") return readScanCache(tradingRegion || "");
|
||||
if (typeof window !== "undefined") {
|
||||
return readScanCache(tradingRegion || "", { allowStale: true });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
setCachedRows(readScanCache(tradingRegion || "", { allowStale: true }));
|
||||
}, [tradingRegion]);
|
||||
|
||||
const fetchScanTerminal = useCallback(
|
||||
async ({
|
||||
forceRefresh = false,
|
||||
|
||||
Reference in New Issue
Block a user