"use client";
import clsx from "clsx";
import Link from "next/link";
import {
Activity,
BarChart3,
Bell,
CloudSun,
CreditCard,
Gauge,
LineChart,
LockKeyhole,
LogIn,
Menu,
RefreshCw,
Search,
Table2,
UserRound,
} from "lucide-react";
import { 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 { useScanTerminalQuery } from "@/components/dashboard/scan-terminal/use-scan-terminal-query";
import {
useScanTerminalTheme,
useUserLocalClock,
} from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state";
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: "Weather Market Dashboard", zh: "天气市场数据仪表盘" },
refresh: { en: "Refresh", zh: "刷新" },
switchLang: { en: "Switch to Chinese", 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 (
);
}
function MarketTable({
onSelect,
rows,
selectedId,
isEn = true,
}: {
onSelect: (row: ScanOpportunityRow) => void;
rows: ScanOpportunityRow[];
selectedId?: string | null;
isEn?: boolean;
}) {
return (
{t("cityContract", isEn)}
{t("live", isEn)}
{t("deb", isEn)}
{t("mkt", isEn)}
{t("edge", isEn)}
{t("liq", isEn)}
{t("signal", isEn)}
{rows.map((row) => {
const label = decisionLabel(row);
return (
onSelect(row)}
>
{rowName(row)}
{row.target_label || row.market_question || row.airport || "--"}
{temp(row.current_max_so_far ?? row.current_temp, row.temp_symbol)}
{temp(row.deb_prediction, row.temp_symbol)}
{pct(row.market_probability ?? row.market_event_probability)}
{pct(row.edge_percent ?? row.signed_gap ?? row.gap)}
{money(row.book_liquidity ?? row.market_liquidity ?? row.volume)}
{label}
);
})}
);
}
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 topRows = rows.slice(0, 18);
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 = decisionLabel(selectedRow);
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 (
{[CloudSun, Table2, BarChart3, LineChart, Gauge, Bell].map((Icon, i) => (
))}
{t("searchPlaceholder", isEn)}
/
{userLocalTime}
{/* Language toggle — matches landing page style */}
中文
EN
{t("refresh", isEn)}
{t("rows", isEn)}
{rows.length}
{t("avgEdge", isEn)}
{pct(avgEdge)}
{t("liquidity", isEn)}
{money(totalLiquidity)}
{(approveRows.length ? approveRows : topRows.slice(0, 5)).map((row) => (
setSelectedRow(row)}
className="grid w-full grid-cols-[1fr_auto] items-center gap-3 px-3 py-2 text-left hover:bg-blue-50"
>
{rowName(row)}
{row.ai_reason_en || row.ai_city_thesis_en || row.target_label || "--"}
{pct(row.edge_percent)}
))}
{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]) => (
))}
{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 : topRows.slice(0, 8)).map((row) => (
setSelectedRow(row)}
className="grid w-full grid-cols-[1fr_auto] gap-3 px-3 py-2 text-left hover:bg-blue-50"
>
{rowName(row)}
{row.airport || row.trading_region_label || row.local_time || "--"}
{pct(row.signed_gap ?? row.gap)}
))}
{[
["Heat", rows.filter((r) => r.risk_level === "high").length],
["Active", rows.filter((r) => r.active).length],
["Tradable", rows.filter((r) => r.tradable).length],
["Primary", rows.filter((r) => r.is_primary_signal).length],
["AI", rows.filter((r) => r.ai_decision).length],
["Closed", rows.filter((r) => r.closed).length],
].map(([label, value]) => (
))}
Data
{generatedText || "live"}
Access
paid
Layout
Koyfin-style grid
);
}
// ─── 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 }: { isEn: boolean }) {
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 */}
{t("subscribeNow", isEn)}
{isEn
? "Cancel anytime · No hidden fees · Instant access after payment"
: "随时取消 · 无隐藏费用 · 付款后立即解锁"}
{/* Back link */}
← {t("backToProduct", isEn)}
);
}
// ─── 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 (
Terminal
{userLocalTime}
{t("signInToContinue", isEn)}
{isEn
? "The weather terminal is for verified subscribers only."
: "天气决策台仅对已验证的付费用户开放。"}
{isEn ? "Log in" : "登录"}
{isEn ? "Create an account" : "注册账号"}
{isEn ? "← Learn about PolyWeather" : "← 了解 PolyWeather"}
);
}
/** Unified access gate — routes to the correct layer based on auth state */
function ProductAccessRequired({
isAuthenticated,
isEn,
userLocalTime,
}: {
isAuthenticated: boolean;
isEn: boolean;
userLocalTime: string;
}) {
// Layer 1 fallback (middleware should catch this first in production)
if (!isAuthenticated) {
return ;
}
// Layer 2: logged in, no subscription
return (
Terminal
{userLocalTime}
);
}
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();
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 (
);
}
if (!isAuthenticated || !isPro) {
return (
);
}
return (
setSelectedId(row.id)}
toggleLocale={toggleLocale}
userLocalTime={userLocalTime}
/>
);
}
export function ScanTerminalDashboard() {
return ;
}