"use client"; import clsx from "clsx"; import Link from "next/link"; import { Activity, BarChart3, Bell, ChevronDown, ChevronRight, CloudSun, CreditCard, Gauge, LineChart, LockKeyhole, LogIn, Menu, RefreshCw, Search, Table2, UserRound, } from "lucide-react"; import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; import { Area, AreaChart, Bar, BarChart as ReBarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import type { ProAccessState, ScanOpportunityRow } from "@/lib/dashboard-types"; import { getInitialLocaleFromNavigator } from "@/lib/i18n"; import { isBrowserLocalFullAccess } from "@/lib/local-dev-access"; import { sortRowsByUserTime } from "@/components/dashboard/scan-terminal/decision-utils"; import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay"; import { useTerminalPay } from "@/components/subscription/useTerminalPay"; import { type ContinentGroup, buildContinentGroups, formatPrice, formatSpreadLiquidity, GAP_COLOR_MAP, getDefaultExpanded, getGapColor, getSignalLabel, getSignalState, } from "@/components/dashboard/scan-terminal/continent-grouping"; import { MobileCityCard } from "@/components/dashboard/scan-terminal/MobileCityCard"; import { MobileRegionTabs } from "@/components/dashboard/scan-terminal/MobileRegionTabs"; import { useScanTerminalQuery } from "@/components/dashboard/scan-terminal/use-scan-terminal-query"; import { useScanTerminalTheme, useUserLocalClock, } from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state"; import { ScanTerminalLoadingScreen } from "@/components/dashboard/scan-terminal/ScanTerminalShellParts"; import { scanRootClass } from "@/components/dashboard/scan-root-styles"; import { useRelativeTime } from "@/hooks/useRelativeTime"; function createEmptyAccess(loading = true): ProAccessState { return { loading, authenticated: false, userId: null, subscriptionActive: false, subscriptionPlanCode: null, subscriptionExpiresAt: null, subscriptionTotalExpiresAt: null, subscriptionQueuedDays: 0, points: 0, error: null, }; } function createLocalAccess(): ProAccessState { return { loading: false, authenticated: true, userId: "local-dev", subscriptionActive: true, subscriptionPlanCode: "local-full-access", subscriptionExpiresAt: "2099-12-31T23:59:59Z", subscriptionTotalExpiresAt: "2099-12-31T23:59:59Z", subscriptionQueuedDays: 0, points: 999_999, error: null, }; } function rowName(row?: ScanOpportunityRow | null) { return row?.city_display_name || row?.display_name || row?.city || "--"; } function pct(value: unknown, digits = 1) { const n = Number(value); if (!Number.isFinite(n)) return "--"; return `${(Math.abs(n) <= 1 ? n * 100 : n).toFixed(digits)}%`; } function money(value: unknown) { const n = Number(value); if (!Number.isFinite(n)) return "--"; return `$${Math.round(n).toLocaleString()}`; } function temp(value: unknown, unit?: string | null) { const n = Number(value); if (!Number.isFinite(n)) return "--"; return `${n.toFixed(1)}${unit || "°"}`; } function edgeClass(value: unknown) { const n = Number(value); if (!Number.isFinite(n) || n === 0) return "text-slate-500"; return n > 0 ? "text-emerald-700" : "text-red-600"; } const TERM = { cityContract: { en: "City / Contract", zh: "城市 / 合约" }, live: { en: "Live", zh: "实测" }, deb: { en: "DEB", zh: "DEB" }, mkt: { en: "Mkt", zh: "市场" }, edge: { en: "Edge", zh: "优势" }, liq: { en: "Liq", zh: "流动性" }, signal: { en: "Signal", zh: "信号" }, searchPlaceholder: { en: "Search city, contract, station, or signal", zh: "搜索城市、合约、站点或信号" }, weatherContracts: { en: "Weather Contracts", zh: "天气合约" }, approvedSignals: { en: "Approved Signals", zh: "已确认信号" }, selectedContractMonitor: { en: "Selected Contract Monitor", zh: "选中合约监控" }, probabilityDistribution: { en: "Probability Distribution", zh: "概率分布" }, marketList: { en: "Market List", zh: "市场列表" }, watchlist: { en: "Watchlist", zh: "观察列表" }, rows: { en: "Rows", zh: "行数" }, avgEdge: { en: "Avg Edge", zh: "平均优势" }, liquidity: { en: "Liquidity", zh: "流动性" }, intradayPerformance: { en: "Intraday Performance", zh: "日内表现" }, spread: { en: "Spread", zh: "价差" }, model: { en: "Model", zh: "模型" }, noData: { en: "No data", zh: "无数据" }, noDistributionData: { en: "No distribution data", zh: "无分布数据" }, selectContract: { en: "Select a weather contract to inspect model edge, market price, and live evidence.", zh: "选择天气合约以查看模型优势、市场价格和实况证据。", }, signInToContinue: { en: "Sign in to continue", zh: "请先登录" }, signInHint: { en: "The terminal is only available to registered users. Please sign in or create an account.", zh: "决策台仅对注册用户开放。请登录或创建账号。", }, logIn: { en: "Log in", zh: "登录" }, createAccount: { en: "Create an account", zh: "注册账号" }, learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" }, proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" }, proDesc: { en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-market intelligence.", zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气市场情报。", }, subscriptionTerms: { en: "Billed monthly. Cancel anytime. Payment via USDC on Polygon.", zh: "按月计费,随时可取消。通过 Polygon 链 USDC 支付。", }, month: { en: "/ month", zh: "/ 月" }, subscribeNow: { en: "Subscribe Now — $10/mo", zh: "立即订阅 — $10/月" }, subscribePrompt: { en: "You need an active subscription to access the terminal.", zh: "你需要开通有效订阅才能访问决策台。", }, backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" }, dashboard: { en: "PolyWeather Terminal", zh: "PolyWeather 交易决策台" }, refresh: { en: "Refresh", zh: "刷新" }, switchLang: { en: "Switch to Chinese", zh: "切换到英文" }, globalWeatherFactors: { en: "Global Weather Factors", zh: "全球天气因子" }, heat: { en: "Heat", zh: "高温风险" }, active: { en: "Active", zh: "活跃" }, watch: { en: "Watch", zh: "观察" }, tradable: { en: "Tradable", zh: "可交易" }, primary: { en: "Primary", zh: "主信号" }, ai: { en: "AI", zh: "AI" }, closed: { en: "Closed", zh: "已关闭" }, terminalStatus: { en: "Terminal Status", zh: "终端状态" }, tsData: { en: "Data", zh: "数据" }, tsAccess: { en: "Access", zh: "访问权限" }, tsLayout: { en: "Layout", zh: "布局" }, tsLayoutValue: { en: "Koyfin-style grid", zh: "Koyfin 风格网格" }, tsDataLive: { en: "live", zh: "实时" }, tsAccessPaid: { en: "paid", zh: "付费" }, } as const; function t(key: keyof typeof TERM, isEn: boolean) { return isEn ? TERM[key].en : TERM[key].zh; } function decisionLabel(row?: ScanOpportunityRow | null) { const raw = row?.ai_decision || row?.v4_metar_decision || row?.action || row?.signal_status || ""; const value = String(raw || "").toLowerCase(); if (value.includes("approve")) return "Approve"; if (value.includes("veto")) return "Veto"; if (value.includes("watch")) return "Watch"; if (value.includes("downgrade")) return "Downgrade"; if (row?.tradable) return "Tradable"; return "Monitor"; } function decisionClass(label: string) { if (label === "Approve" || label === "Tradable") { return "border-emerald-200 bg-emerald-50 text-emerald-700"; } if (label === "Veto" || label === "Downgrade") { return "border-red-200 bg-red-50 text-red-700"; } return "border-amber-200 bg-amber-50 text-amber-700"; } function SparkArea({ color = "#2563eb", data, isEn = true, }: { color?: string; data: { v: number }[]; isEn?: boolean; }) { if (!data.length) { return (
{t("noData", isEn)}
); } return ( ); } function ProbabilityDistributionChart({ points, isEn = true, }: { points?: ScanOpportunityRow["distribution_preview"]; isEn?: boolean; }) { if (!points?.length) { return (
{t("noDistributionData", isEn)}
); } const chartData = points.map((p) => ({ label: p.label || "", model: Number((p.model_probability ?? 0) * 100), market: Number((p.market_probability ?? 0) * 100), })); return ( `${v}%`} /> `${Number(value).toFixed(1)}%` } /> ); } function Panel({ children, className, title, }: { children: React.ReactNode; className?: string; title: string; }) { return (

{title}

{children}
); } function GroupedMarketTable({ groups, isEn, onSelect, selectedId, }: { groups: ContinentGroup[]; isEn: boolean; onSelect: (row: ScanOpportunityRow) => void; selectedId?: string | null; }) { const [collapsed, setCollapsed] = useState>(() => { const c = new Set(); const defaultExpanded = getDefaultExpanded(groups); for (const g of groups) { if (!defaultExpanded.has(g.key)) c.add(g.key); } return c; }); const toggleGroup = (key: string) => { setCollapsed((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; return (
{groups.map((group) => { const isExpanded = !collapsed.has(group.key); const label = isEn ? group.labelEn : group.labelZh; return ( {/* Group header row */} {/* Data rows */} {isExpanded && group.rows.map((row) => { const signal = getSignalState(row); const gapColor = GAP_COLOR_MAP[getGapColor(row)]; return ( onSelect(row)} > ); })} ); })}
City Obs High DEB Gap Market Edge Spr/Liq Signal
{rowName(row)}
{row.airport || ""}{row.local_time ? ` · ${row.local_time}` : ""}
{temp(row.current_temp, row.temp_symbol)} {temp(row.current_max_so_far, row.temp_symbol)} {temp(row.deb_prediction, row.temp_symbol)} {temp(row.signed_gap ?? row.gap_to_target, row.temp_symbol)} {formatPrice(row.midpoint, row.ask, row.bid)} {pct(row.edge_percent)} {formatSpreadLiquidity(row.spread, row.book_liquidity ?? row.market_liquidity)} {getSignalLabel(signal, isEn)}
); } function KoyfinWeatherTerminal({ generatedText, isEn, locale, onRefresh, refreshing, rows, selectedRow, setSelectedRow, toggleLocale, userLocalTime, }: { generatedText: string; isEn: boolean; locale: "zh-CN" | "en-US"; onRefresh: () => void; refreshing: boolean; rows: ScanOpportunityRow[]; selectedRow: ScanOpportunityRow | null; setSelectedRow: (row: ScanOpportunityRow) => void; toggleLocale: () => void; userLocalTime: string; }) { const approveRows = rows .filter((row) => ["Approve", "Tradable"].includes(decisionLabel(row))) .slice(0, 8); const watchRows = rows .filter((row) => decisionLabel(row) === "Watch" || !row.tradable) .slice(0, 8); const selectedLabel = selectedRow ? getSignalLabel(getSignalState(selectedRow), isEn) : ""; const continentGroups = useMemo( () => buildContinentGroups(rows, isEn), [rows, isEn] ); const [mobileTab, setMobileTab] = useState("active_signals"); const mobileActiveGroup = useMemo( () => continentGroups.find((g) => g.key === mobileTab) || continentGroups[0], [continentGroups, mobileTab] ); useEffect(() => { if (continentGroups.length > 0 && !continentGroups.find((g) => g.key === mobileTab)) { setMobileTab(continentGroups[0].key); } }, [continentGroups, mobileTab]); const avgEdge = rows.reduce((sum, row) => sum + Number(row.edge_percent || 0), 0) / Math.max(rows.length, 1); const totalLiquidity = rows.reduce( (sum, row) => sum + Number(row.book_liquidity || row.market_liquidity || row.volume || 0), 0, ); return (
{t("searchPlaceholder", isEn)} /
{t("dashboard", isEn)}
{userLocalTime} {/* Language toggle — matches landing page style */}
{/* Mobile layout */}
{mobileActiveGroup?.rows.map((row) => ( ))}
{/* Mobile Selected Row Detail */} {selectedRow && (

{rowName(selectedRow)}

{[ ["Obs", temp(selectedRow.current_temp, selectedRow.temp_symbol)], ["High", temp(selectedRow.current_max_so_far, selectedRow.temp_symbol)], ["DEB", temp(selectedRow.deb_prediction, selectedRow.temp_symbol)], ["Gap", temp(selectedRow.signed_gap ?? selectedRow.gap_to_target, selectedRow.temp_symbol)], ["Edge", pct(selectedRow.edge_percent)], ["Market", formatPrice(selectedRow.midpoint, selectedRow.ask, selectedRow.bid)], ].map(([label, value]) => (
{label}
{value}
))}
)}
{/* Desktop layout */}
{t("rows", isEn)}
{rows.length}
{t("avgEdge", isEn)}
{pct(avgEdge)}
{t("liquidity", isEn)}
{money(totalLiquidity)}
{(approveRows.length ? approveRows : rows.slice(0, 5)).map((row) => ( ))}

{rowName(selectedRow)}

{selectedLabel}

{selectedRow?.ai_city_thesis_en || selectedRow?.ai_reason_en || selectedRow?.market_question || t("selectContract", isEn)}

{[ [t("live", isEn), temp(selectedRow?.current_max_so_far ?? selectedRow?.current_temp, selectedRow?.temp_symbol)], [t("deb", isEn), temp(selectedRow?.deb_prediction, selectedRow?.temp_symbol)], [t("model", isEn), pct(selectedRow?.model_probability ?? selectedRow?.model_event_probability)], [t("mkt", isEn), pct(selectedRow?.market_probability ?? selectedRow?.market_event_probability)], ].map(([label, value]) => (
{label}
{value}
))}
{t("intradayPerformance", isEn)}
= 0 ? "#059669" : "#dc2626" } data={ selectedRow?.distribution_preview?.map((p) => ({ v: typeof p === "number" ? p : p.model_probability ?? 0, })) || [] } />
{t("edge", isEn)} {pct(selectedRow?.edge_percent)} {t("spread", isEn)} {pct(selectedRow?.spread)}
{(watchRows.length ? watchRows : rows.slice(0, 8)).map((row) => ( ))}
{[ [t("heat", isEn), rows.filter((r) => r.risk_level === "high").length], [t("active", isEn), rows.filter((r) => r.active).length], [t("tradable", isEn), rows.filter((r) => r.tradable).length], [t("primary", isEn), rows.filter((r) => r.is_primary_signal).length], [t("ai", isEn), rows.filter((r) => r.ai_decision).length], [t("closed", isEn), rows.filter((r) => r.closed).length], ].map(([label, value]) => (
{value}
{label}
))}
{t("tsData", isEn)} {generatedText || t("tsDataLive", isEn)}
{t("tsAccess", isEn)} {t("tsAccessPaid", isEn)}
{t("tsLayout", isEn)} {t("tsLayoutValue", isEn)}
); } // ─── Layer 2: Authenticated but no active subscription ─────────────────────── // Shown inside the terminal shell after middleware has confirmed the user is // logged in (Layer 1). Presents a targeted upgrade prompt. function SubscriptionGate({ isEn, points, onPaid, }: { isEn: boolean; points: number; onPaid: () => void; }) { const [showOverlay, setShowOverlay] = useState(false); const { billing, usePoints, setUsePoints, payBusy, errorText, infoText, txHash, chainId, tokenSymbol, walletAddress, handlePay, } = useTerminalPay({ points, planPriceUsd: 10, onPaid: () => { setShowOverlay(false); onPaid(); }, isEn, }); const handleSubscribeClick = () => { setShowOverlay(true); }; const features = isEn ? [ "Real-time METAR observations across 500+ stations", "DEB forecast blends with 0–240h horizon", "AI decision cards with Poly-score ranking", "Historical backtesting & weather market signals", ] : [ "500+ 气象站实时 METAR 实况", "DEB 智能融合预测(0–240 小时)", "AI 决策卡片 + Poly-score 排名", "历史回测与天气市场交易信号", ]; return (
{/* Header badge */}
{t("proAccessRequired", isEn)}
{/* Main card */}
{/* Card top band */}

{isEn ? "Unlock the Weather Terminal" : "解锁天气交易决策台"}

{isEn ? "Your account is verified. One step away from full access." : "账号已验证,只差一步即可获得完整访问权限。"}

{/* Price */}
$10 {t("month", isEn)}
{/* Feature list */}
    {features.map((f) => (
  • {f}
  • ))}
{/* CTA */}

{isEn ? "Cancel anytime · No hidden fees · Instant access after payment" : "随时取消 · 无隐藏费用 · 付款后立即解锁"}

{/* Back link */}
← {t("backToProduct", isEn)}
{/* Payment overlay */} {showOverlay && (
setUsePoints((prev) => !prev)} onPay={() => void handlePay()} onClose={() => setShowOverlay(false)} payBusy={payBusy} payLabel={ walletAddress ? isEn ? "Subscribe & Activate" : "立即订阅并激活服务" : isEn ? "Connect Wallet & Subscribe" : "连接钱包并订阅" } errorText={errorText || undefined} infoText={infoText || undefined} txHash={txHash || undefined} chainId={chainId} paymentTokenLabel={tokenSymbol} locale={isEn ? "en-US" : "zh-CN"} faqHref="/subscription-help" />
)}
); } // ─── Layer 1 fallback: Should not normally appear (middleware handles it) ───── // Shown only when middleware is not configured (no Supabase env). function UnauthenticatedGate({ isEn, userLocalTime, }: { isEn: boolean; userLocalTime: string; }) { return (
); } /** Unified access gate — routes to the correct layer based on auth state */ function ProductAccessRequired({ isAuthenticated, isEn, points, onPaid, userLocalTime, }: { isAuthenticated: boolean; isEn: boolean; points: number; onPaid: () => void; userLocalTime: string; }) { // Layer 1 fallback (middleware should catch this first in production) if (!isAuthenticated) { return ; } // Layer 2: logged in, no subscription — inline pay overlay return (
); } function ScanTerminalScreen() { const [proAccess, setProAccess] = useState(() => createEmptyAccess(true), ); const [locale, setLocale] = useState<"zh-CN" | "en-US">("zh-CN"); const isEn = locale === "en-US"; const toggleLocale = () => setLocale((prev) => (prev === "zh-CN" ? "en-US" : "zh-CN")); const [hydrated, setHydrated] = useState(false); const [localFullAccess, setLocalFullAccess] = useState(false); const canUseLocalFullAccess = hydrated && localFullAccess; const isAuthenticated = hydrated && (proAccess.authenticated || canUseLocalFullAccess); const isPro = hydrated && (proAccess.subscriptionActive || canUseLocalFullAccess); const userLocalTime = useUserLocalClock(); const { themeMode } = useScanTerminalTheme(); useEffect(() => { let cancelled = false; setHydrated(true); setLocale(getInitialLocaleFromNavigator()); const localAccess = isBrowserLocalFullAccess(); setLocalFullAccess(localAccess); if (localAccess) { setProAccess(createLocalAccess()); return () => { cancelled = true; }; } if (typeof fetch !== "function") { setProAccess(createEmptyAccess(false)); return () => { cancelled = true; }; } fetch("/api/auth/me", { cache: "no-store", headers: { Accept: "application/json" }, }) .then(async (response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise<{ authenticated?: boolean; user_id?: string | null; subscription_active?: boolean | null; subscription_plan_code?: string | null; subscription_expires_at?: string | null; subscription_total_expires_at?: string | null; subscription_queued_days?: number | null; points?: number | null; }>; }) .then((payload) => { if (cancelled) return; setProAccess({ loading: false, authenticated: Boolean(payload.authenticated), userId: payload.user_id ?? null, subscriptionActive: payload.subscription_active === true, subscriptionPlanCode: payload.subscription_plan_code ?? null, subscriptionExpiresAt: payload.subscription_expires_at ?? null, subscriptionTotalExpiresAt: payload.subscription_total_expires_at ?? payload.subscription_expires_at ?? null, subscriptionQueuedDays: Math.max( 0, Number(payload.subscription_queued_days ?? 0), ), points: Number(payload.points ?? 0), error: null, }); }) .catch((error) => { if (cancelled) return; setProAccess({ ...createEmptyAccess(false), error: String(error), }); }); return () => { cancelled = true; }; }, []); const { refreshScanTerminalManually, scanLoading, terminalData } = useScanTerminalQuery({ isPro, proAccessLoading: !hydrated || (proAccess.loading && !canUseLocalFullAccess), }); const rows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), [terminalData?.rows], ); const [selectedId, setSelectedId] = useState(null); const selectedRow = useMemo( () => rows.find((row) => row.id === selectedId) || rows[0] || null, [rows, selectedId], ); const generatedText = useRelativeTime(terminalData?.generated_at ?? null); if (!hydrated || (proAccess.loading && !canUseLocalFullAccess)) { return ( ); } const refreshAuth = useCallback(() => { fetch("/api/auth/me", { cache: "no-store", headers: { Accept: "application/json" }, }) .then(async (response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise<{ authenticated?: boolean; user_id?: string | null; subscription_active?: boolean | null; subscription_plan_code?: string | null; subscription_expires_at?: string | null; subscription_total_expires_at?: string | null; subscription_queued_days?: number | null; points?: number | null; }>; }) .then((payload) => { setProAccess({ loading: false, authenticated: Boolean(payload.authenticated), userId: payload.user_id ?? null, subscriptionActive: payload.subscription_active === true, subscriptionPlanCode: payload.subscription_plan_code ?? null, subscriptionExpiresAt: payload.subscription_expires_at ?? null, subscriptionTotalExpiresAt: payload.subscription_total_expires_at ?? payload.subscription_expires_at ?? null, subscriptionQueuedDays: Math.max( 0, Number(payload.subscription_queued_days ?? 0), ), points: Number(payload.points ?? 0), error: null, }); }) .catch(() => {}); // keep current state on error }, []); if (!isAuthenticated || !isPro) { return ( ); } return ( setSelectedId(row.id)} toggleLocale={toggleLocale} userLocalTime={userLocalTime} /> ); } export function ScanTerminalDashboard() { return ; }