diff --git a/frontend/app/api/city/[name]/market-scan/route.ts b/frontend/app/api/city/[name]/market-scan/route.ts deleted file mode 100644 index 651c27b5..00000000 --- a/frontend/app/api/city/[name]/market-scan/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { proxyBackendJsonGet } from "@/lib/api-proxy"; -import { buildForceRefreshProxyCachePolicy } from "@/lib/proxy-cache-policy"; - -const API_BASE = process.env.POLYWEATHER_API_BASE_URL; - -export async function GET( - req: NextRequest, - context: { params: Promise<{ name: string }> }, -) { - if (!API_BASE) { - const response = NextResponse.json( - { error: "POLYWEATHER_API_BASE_URL is not configured" }, - { status: 500 }, - ); - return response; - } - - const { name } = await context.params; - const params = new URLSearchParams(); - const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false"; - params.set("force_refresh", forceRefresh); - const cachePolicy = buildForceRefreshProxyCachePolicy(forceRefresh, 20); - - const targetDate = req.nextUrl.searchParams.get("target_date"); - if (targetDate) { - params.set("target_date", targetDate); - } - - const marketSlug = req.nextUrl.searchParams.get("market_slug"); - if (marketSlug) { - params.set("market_slug", marketSlug); - } - - const lite = req.nextUrl.searchParams.get("lite"); - if (lite) { - params.set("lite", lite); - } - - const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`; - - return proxyBackendJsonGet(req, { - cacheControl: cachePolicy.responseCacheControl, - detailLimit: 800, - error: "Backend city market scan failed", - fetchCache: - cachePolicy.fetchMode === "no-store" ? "no-store" : undefined, - publicMessage: "Failed to fetch city market scan", - revalidateSeconds: cachePolicy.revalidateSeconds, - statusOnException: 502, - url, - }); -} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 467f40c8..554f0217 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -45,7 +45,6 @@ import { Panel } from "@/components/dashboard/scan-terminal/Panel"; import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard"; import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart"; import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable"; -import { MarketOverviewView } from "@/components/dashboard/scan-terminal/MarketOverviewView"; import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils"; function createEmptyAccess(loading = true): ProAccessState { diff --git a/frontend/components/dashboard/scan-terminal/MarketDecisionLine.tsx b/frontend/components/dashboard/scan-terminal/MarketDecisionLine.tsx deleted file mode 100644 index 1680156a..00000000 --- a/frontend/components/dashboard/scan-terminal/MarketDecisionLine.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import clsx from "clsx"; -import type { MarketDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils"; - -export function MarketDecisionLine({ - isEn, - marketDecisionView, - marketLineText, -}: { - isEn: boolean; - marketDecisionView: MarketDecisionView; - marketLineText: string; -}) { - return ( - <> -
- {isEn ? "Market price" : "市场价格"} - {marketLineText} -
-
-
- {isEn ? "Polymarket price layer" : "Polymarket 价格层"} - {marketDecisionView.title} -

{marketDecisionView.reason}

-
-
- - {isEn ? "Bucket" : "温度桶"} {marketDecisionView.bucketLabel} - - - {isEn ? "YES buy" : "YES 买价"} {marketDecisionView.priceText} - - - {isEn ? "Model-market" : "模型-市场差"} {marketDecisionView.edgeText} - -
- {marketDecisionView.marketUrl ? ( - - {isEn ? "Open market" : "打开市场"} - - ) : null} -
- - ); -} diff --git a/frontend/components/dashboard/scan-terminal/MarketOverviewView.tsx b/frontend/components/dashboard/scan-terminal/MarketOverviewView.tsx deleted file mode 100644 index 6bfd29dd..00000000 --- a/frontend/components/dashboard/scan-terminal/MarketOverviewView.tsx +++ /dev/null @@ -1,361 +0,0 @@ -"use client"; - -import clsx from "clsx"; -import { useEffect, useMemo, useState } from "react"; -import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types"; -import { - resolveTradingRegionKey, - TRADING_REGIONS, -} from "@/components/dashboard/scan-terminal/continent-grouping"; -import { Panel } from "@/components/dashboard/scan-terminal/Panel"; -import { edgeClass, money, pct, rowName } from "@/components/dashboard/scan-terminal/utils"; - -type Holder = { - amount?: number; - name?: string; - outcomeIndex?: number; - proxyWallet?: string; - pseudonym?: string; -}; - -type HolderInfo = { - holders: Holder[] | null; - loading: boolean; -}; - -const MARKET_OVERVIEW_REFRESH_MS = 10 * 60_000; - -function numeric(value: unknown) { - return typeof value === "number" && Number.isFinite(value) ? value : 0; -} - -function rowLiquidity(row: ScanOpportunityRow) { - return numeric(row.book_liquidity || row.market_liquidity || row.volume); -} - -function rowOpportunityScore(row: ScanOpportunityRow) { - return ( - numeric(row.final_score) * 0.5 + - numeric(row.edge_percent ?? row.signed_gap ?? row.gap) * 2 + - Math.log10(rowLiquidity(row) + 1) - ); -} - -function rowSpread(row: ScanOpportunityRow) { - return numeric(row.spread); -} - -function isStale(row: ScanOpportunityRow) { - return Boolean(row.metar_context?.stale_for_today || row.metar_status?.stale_for_today); -} - -function isWatch(row: ScanOpportunityRow) { - const value = String(row.ai_decision || row.v4_metar_decision || row.signal_status || row.action || "").toLowerCase(); - return value.includes("watch") || !row.tradable; -} - -function holderLabel(holder: Holder) { - return ( - holder.name || - holder.pseudonym || - (holder.proxyWallet ? `${String(holder.proxyWallet).slice(0, 6)}...${String(holder.proxyWallet).slice(-4)}` : "--") - ); -} - -function CompactRowsTable({ - empty, - isEn, - onSelectRow, - rows, - showSpread = false, -}: { - empty: string; - isEn: boolean; - onSelectRow: (row: ScanOpportunityRow) => void; - rows: ScanOpportunityRow[]; - showSpread?: boolean; -}) { - if (!rows.length) { - return
{empty}
; - } - return ( - - - - - - - - - - - - {rows.map((row) => { - const edge = numeric(row.edge_percent ?? row.signed_gap ?? row.gap); - return ( - onSelectRow(row)} - className="cursor-pointer border-b border-slate-100 hover:bg-blue-50" - > - - - - - - - ); - })} - -
{isEn ? "City / Contract" : "城市 / 合约"}{isEn ? "Model" : "模型"}{isEn ? "Market" : "市场"}{showSpread ? (isEn ? "Spread" : "价差") : "Edge"}{isEn ? "Liq" : "流动性"}
-
{rowName(row)}
-
{row.target_label || row.market_question || "--"}
-
{pct(row.model_probability ?? row.model_event_probability)}{pct(row.market_probability ?? row.market_event_probability)} - {showSpread ? pct(row.spread) : pct(edge)} - {money(rowLiquidity(row))}
- ); -} - -export function MarketOverviewView({ - isEn, - onSelectRow, - rows, -}: { - isEn: boolean; - onSelectRow: (row: ScanOpportunityRow) => void; - rows: ScanOpportunityRow[]; -}) { - const [overviewRows, setOverviewRows] = useState(rows); - const [lastScanAt, setLastScanAt] = useState(null); - - useEffect(() => { - setOverviewRows(rows); - }, [rows]); - - useEffect(() => { - let cancelled = false; - let controller: AbortController | null = null; - - const refreshOverview = async () => { - if (typeof fetch !== "function" || typeof AbortController === "undefined") return; - controller?.abort(); - controller = new AbortController(); - try { - const response = await fetch("/api/scan/terminal?force_refresh=false", { - cache: "no-store", - headers: { Accept: "application/json" }, - signal: controller.signal, - }); - if (!response.ok) return; - const payload = await response.json() as ScanTerminalResponse; - if (!cancelled && Array.isArray(payload.rows)) { - setOverviewRows(payload.rows); - setLastScanAt(new Date()); - } - } catch (error) { - if ((error as { name?: string })?.name !== "AbortError") { - // Keep the existing snapshot; overview refresh should never blank the terminal. - } - } - }; - - const intervalId = window.setInterval(() => { - void refreshOverview(); - }, MARKET_OVERVIEW_REFRESH_MS); - - return () => { - cancelled = true; - controller?.abort(); - window.clearInterval(intervalId); - }; - }, []); - - const primaryRows = useMemo( - () => overviewRows.filter((row) => row.is_primary_signal !== false), - [overviewRows], - ); - const regionStats = useMemo( - () => - TRADING_REGIONS.map((region) => { - const list = primaryRows.filter((row) => resolveTradingRegionKey(row) === region.key); - const avgEdge = list.reduce((sum, row) => sum + numeric(row.edge_percent ?? row.signed_gap ?? row.gap), 0) / Math.max(list.length, 1); - return { - key: region.key, - label: isEn ? region.labelEn : region.labelZh, - contracts: list.length, - tradable: list.filter((row) => row.tradable).length, - heat: list.filter((row) => row.risk_level === "high" || numeric(row.current_temp) >= 30).length, - avgEdge, - liquidity: list.reduce((sum, row) => sum + rowLiquidity(row), 0), - }; - }).filter((item) => item.contracts > 0), - [isEn, primaryRows], - ); - - const topOpportunities = useMemo( - () => [...primaryRows].sort((a, b) => rowOpportunityScore(b) - rowOpportunityScore(a)).slice(0, 12), - [primaryRows], - ); - const riskRows = useMemo( - () => - primaryRows - .filter((row) => isWatch(row) || numeric(row.edge_percent ?? row.signed_gap ?? row.gap) < 0 || row.closed || isStale(row)) - .sort((a, b) => numeric(a.edge_percent ?? a.signed_gap ?? a.gap) - numeric(b.edge_percent ?? b.signed_gap ?? b.gap)) - .slice(0, 12), - [primaryRows], - ); - const liquidityRows = useMemo( - () => [...primaryRows].sort((a, b) => rowLiquidity(b) - rowLiquidity(a)).slice(0, 8), - [primaryRows], - ); - const tightSpreadRows = useMemo( - () => - [...primaryRows] - .filter((row) => rowSpread(row) > 0) - .sort((a, b) => rowSpread(a) - rowSpread(b)) - .slice(0, 8), - [primaryRows], - ); - const whaleRows = useMemo( - () => liquidityRows.slice(0, 6), - [liquidityRows], - ); - - const [holderMap, setHolderMap] = useState>({}); - useEffect(() => { - whaleRows.forEach((row) => { - const city = String(row.city || "").toLowerCase(); - if (!city || holderMap[city]?.loading || holderMap[city]?.holders) return; - setHolderMap((prev) => ({ ...prev, [city]: { holders: null, loading: true } })); - fetch(`/api/city/${encodeURIComponent(city)}/holders?limit=6`, { - cache: "no-store", - headers: { Accept: "application/json" }, - }) - .then(async (res) => { - const json = await res.json() as { holders?: Holder[] }; - setHolderMap((prev) => ({ ...prev, [city]: { holders: json.holders || [], loading: false } })); - }) - .catch(() => setHolderMap((prev) => ({ ...prev, [city]: { holders: null, loading: false } }))); - }); - }, [holderMap, whaleRows]); - - return ( -
- - {isEn ? "Scan: 10m" : "扫描:10分钟"} - {lastScanAt ? ` · ${lastScanAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : ""} - - } - > -
- {regionStats.map((item) => ( - - ))} -
-
- - - - - - - - - - - - - -
- - - - -
- {whaleRows.map((row) => { - const city = String(row.city || "").toLowerCase(); - const info = holderMap[city]; - return ( - - ); - })} -
-
-
-
- ); -} diff --git a/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts deleted file mode 100644 index 3a47898b..00000000 --- a/frontend/components/dashboard/scan-terminal/__tests__/marketScanState.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import assert from "node:assert/strict"; -import { - buildCityMarketScanCacheKey, - deriveCityMarketScanView, - normalizeCityMarketScanPayload, - resolveCityMarketScanSnapshot, - writeCachedCityMarketScan, -} from "@/components/dashboard/scan-terminal/market-scan-state"; -import type { RemoteData } from "@/components/dashboard/scan-terminal/scan-terminal-client"; -import type { CityDetail, MarketScan } from "@/lib/dashboard-types"; - -function installLocalStorageMock() { - const store = new Map(); - const localStorage = { - clear: () => store.clear(), - getItem: (key: string) => store.get(key) ?? null, - removeItem: (key: string) => { - store.delete(key); - }, - setItem: (key: string, value: string) => { - store.set(key, value); - }, - }; - Object.defineProperty(globalThis, "window", { - configurable: true, - value: { localStorage }, - }); - return localStorage; -} - -function marketScan(label = "cached"): MarketScan { - return { - generated_at: "2026-04-28T00:00:00Z", - label, - } as unknown as MarketScan; -} - -function cityDetail(extra: Partial = {}): CityDetail { - return { - local_date: "2026-04-28", - name: "Test City", - ...extra, - } as CityDetail; -} - -export function runTests() { - const storage = installLocalStorageMock(); - storage.clear(); - - const embeddedScan = marketScan("embedded"); - const embeddedSnapshot = resolveCityMarketScanSnapshot({ - detail: cityDetail({ market_scan: embeddedScan }), - detailCityName: "Test City", - enabled: true, - }); - assert.equal(embeddedSnapshot.action, "success"); - if (embeddedSnapshot.action === "success") { - assert.equal(embeddedSnapshot.payload, embeddedScan); - assert.equal(embeddedSnapshot.shouldWriteCache, true); - } - - const cacheKey = buildCityMarketScanCacheKey({ - detailCityName: "Test City", - localDate: "2026-04-28", - }); - const cachedScan = marketScan("cached"); - writeCachedCityMarketScan(cacheKey, cachedScan); - const cachedSnapshot = resolveCityMarketScanSnapshot({ - detail: cityDetail(), - detailCityName: "Test City", - enabled: false, - }); - assert.equal(cachedSnapshot.action, "success"); - if (cachedSnapshot.action === "success") { - assert.equal((cachedSnapshot.payload as unknown as { label: string }).label, "cached"); - } - - storage.clear(); - writeCachedCityMarketScan(cacheKey, { - market_scan: { - available: true, - market_price: 0.41, - selected_slug: "highest-temperature-in-nyc-on-april-30-2026-60-61f", - }, - selected_date: "2026-04-30", - } as unknown as MarketScan); - const wrappedCachedSnapshot = resolveCityMarketScanSnapshot({ - detail: cityDetail(), - detailCityName: "Test City", - enabled: true, - }); - assert.equal(wrappedCachedSnapshot.action, "success"); - if (wrappedCachedSnapshot.action === "success") { - assert.equal(wrappedCachedSnapshot.payload.available, true); - assert.equal(wrappedCachedSnapshot.payload.market_price, 0.41); - assert.equal(wrappedCachedSnapshot.payload.selected_date, "2026-04-30"); - } - - const wrappedResponse = normalizeCityMarketScanPayload({ - market_scan: { available: true, yes_buy: 0.42 } as MarketScan, - selected_date: "2026-04-30", - }); - assert.equal(wrappedResponse?.available, true); - assert.equal(wrappedResponse?.yes_buy, 0.42); - assert.equal(wrappedResponse?.selected_date, "2026-04-30"); - - storage.clear(); - assert.equal( - resolveCityMarketScanSnapshot({ - detail: cityDetail(), - detailCityName: "Test City", - enabled: false, - }).action, - "reset", - ); - assert.equal( - resolveCityMarketScanSnapshot({ - detail: cityDetail(), - detailCityName: "Test City", - enabled: true, - }).action, - "fetch", - ); - - const previous = marketScan("previous"); - const loadingRemote: RemoteData = { - previous, - status: "loading", - }; - const loadingView = deriveCityMarketScanView({ - detailMarketScan: null, - marketRemote: loadingRemote, - }); - assert.equal(loadingView.marketStatus, "loading"); - assert.equal(loadingView.marketScan, previous); - - const errorWithPrevious = deriveCityMarketScanView({ - detailMarketScan: null, - marketRemote: { - error: "network", - previous, - status: "error", - }, - }); - assert.equal(errorWithPrevious.marketStatus, "ready"); - assert.equal(errorWithPrevious.marketScan, previous); - - const errorWithoutPrevious = deriveCityMarketScanView({ - detailMarketScan: null, - marketRemote: { - error: "network", - status: "error", - }, - }); - assert.equal(errorWithoutPrevious.marketStatus, "failed"); - assert.equal(errorWithoutPrevious.marketScan, null); -} diff --git a/frontend/components/dashboard/scan-terminal/market-scan-state.ts b/frontend/components/dashboard/scan-terminal/market-scan-state.ts deleted file mode 100644 index 2f3fe5d4..00000000 --- a/frontend/components/dashboard/scan-terminal/market-scan-state.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - buildStorageKey, - readCachedPayload, - writeCachedPayload, -} from "@/components/dashboard/scan-terminal/scan-terminal-cache"; -import type { RemoteData } from "@/components/dashboard/scan-terminal/scan-terminal-client"; -import type { CityDetail, MarketScan } from "@/lib/dashboard-types"; -import { normalizeCityKey } from "./decision-utils"; - -const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v4"; -const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000; - -export type CityMarketScanStatus = "idle" | "loading" | "ready" | "failed"; - -export type CityMarketScanApiPayload = - | MarketScan - | { - fetched_at?: string | null; - market_scan?: MarketScan | null; - selected_date?: string | null; - }; - -export type CityMarketScanSnapshot = - | { action: "reset"; cacheKey?: string } - | { action: "success"; cacheKey: string; payload: MarketScan; shouldWriteCache?: boolean } - | { action: "fetch"; cacheKey: string }; - -export function buildCityMarketScanCacheKey({ - detailCityName, - localDate, -}: { - detailCityName: string; - localDate?: string | null; -}) { - return buildStorageKey(CITY_MARKET_SCAN_CACHE_PREFIX, [ - normalizeCityKey(detailCityName), - localDate || "", - "full", - ]); -} - -export function readCachedCityMarketScan(cacheKey: string) { - return normalizeCityMarketScanPayload( - readCachedPayload(cacheKey, CITY_MARKET_SCAN_CACHE_TTL_MS), - ); -} - -export function writeCachedCityMarketScan(cacheKey: string, payload: MarketScan) { - writeCachedPayload(cacheKey, payload); -} - -export function normalizeCityMarketScanPayload( - payload: CityMarketScanApiPayload | null | undefined, -): MarketScan | null { - if (!payload || typeof payload !== "object") return null; - if ("market_scan" in payload) { - const scan = payload.market_scan; - if (!scan || typeof scan !== "object") return null; - return { - ...scan, - selected_date: scan.selected_date ?? payload.selected_date ?? null, - }; - } - return payload as MarketScan; -} - -export function resolveCityMarketScanSnapshot({ - detail, - detailCityName, - enabled, -}: { - detail: CityDetail | null; - detailCityName: string; - enabled: boolean; -}): CityMarketScanSnapshot { - if (!detail) return { action: "reset" }; - const cacheKey = buildCityMarketScanCacheKey({ - detailCityName, - localDate: detail.local_date || "", - }); - if (detail.market_scan) { - return { - action: "success", - cacheKey, - payload: detail.market_scan, - shouldWriteCache: true, - }; - } - const cached = readCachedCityMarketScan(cacheKey); - if (cached) { - return { - action: "success", - cacheKey, - payload: cached, - }; - } - return enabled ? { action: "fetch", cacheKey } : { action: "reset", cacheKey }; -} - -export function deriveCityMarketScanView({ - detailMarketScan, - marketRemote, -}: { - detailMarketScan?: MarketScan | null; - marketRemote: RemoteData; -}) { - const previousMarketScan = - marketRemote.status === "loading" || marketRemote.status === "error" - ? marketRemote.previous ?? null - : null; - const marketScan = - marketRemote.status === "success" - ? marketRemote.data - : previousMarketScan ?? detailMarketScan ?? null; - const marketStatus: CityMarketScanStatus = - marketRemote.status === "success" - ? "ready" - : marketRemote.status === "loading" - ? "loading" - : marketRemote.status === "error" - ? marketScan - ? "ready" - : "failed" - : "idle"; - - return { marketScan, marketStatus }; -} diff --git a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts index 9e0b9eb0..d93a8262 100644 --- a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts +++ b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts @@ -1,10 +1,6 @@ "use client"; import { extractStreamingAirportRead } from "@/components/dashboard/scan-terminal/ai-city-stream"; -import { - normalizeCityMarketScanPayload, - type CityMarketScanApiPayload, -} from "@/components/dashboard/scan-terminal/market-scan-state"; import type { AiCityForecastPayload } from "@/components/dashboard/scan-terminal/types"; import { buildBrowserBackendHeaders, @@ -65,10 +61,6 @@ type CityDetailQueryOptions = { targetDate?: string | null; }; -type MarketScanQueryOptions = CityDetailQueryOptions & { - lite?: boolean; -}; - type AiCityReadOptions = { city: string; forceRefresh?: boolean; @@ -308,30 +300,6 @@ async function getCityDetail(city: string, options: CityDetailQueryOptions = {}) ); } -async function getMarketScan(city: string, options: MarketScanQueryOptions = {}) { - const params = new URLSearchParams({ - force_refresh: String(options.forceRefresh ?? false), - }); - if (options.targetDate) params.set("target_date", options.targetDate); - if (options.marketSlug) params.set("market_slug", options.marketSlug); - if (options.lite != null) params.set("lite", String(options.lite)); - const headers = await buildBrowserBackendHeaders({ Accept: "application/json" }); - const payload = await readJsonOrThrow( - `/api/city/${encodeURIComponent(city)}/market-scan?${params.toString()}`, - { - cache: "no-store", - headers, - signal: options.signal, - }, - ); - return ( - normalizeCityMarketScanPayload(payload) ?? { - available: false, - reason: "No market scan payload returned.", - } - ); -} - function runQueuedAiCityReadTask(task: () => Promise, onQueued?: () => void) { return new Promise((resolve, reject) => { const run = () => { @@ -432,7 +400,6 @@ function streamAiCityRead(options: AiCityReadOptions) { export const scanTerminalClient = { getCityDetail, - getMarketScan, getTerminal, streamAiCityRead, }; diff --git a/frontend/components/dashboard/scan-terminal/use-ai-city-card-data.ts b/frontend/components/dashboard/scan-terminal/use-ai-city-card-data.ts deleted file mode 100644 index 304cd914..00000000 --- a/frontend/components/dashboard/scan-terminal/use-ai-city-card-data.ts +++ /dev/null @@ -1,4 +0,0 @@ -"use client"; - -export { useAiCityForecast } from "./use-ai-city-forecast"; -export { useCityMarketScan } from "./use-city-market-scan"; diff --git a/frontend/components/dashboard/scan-terminal/use-city-market-scan.ts b/frontend/components/dashboard/scan-terminal/use-city-market-scan.ts deleted file mode 100644 index 19c7083c..00000000 --- a/frontend/components/dashboard/scan-terminal/use-city-market-scan.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { scanTerminalClient } from "@/components/dashboard/scan-terminal/scan-terminal-client"; -import { useRemoteDataQuery } from "@/components/dashboard/scan-terminal/use-remote-data-query"; -import type { CityDetail, MarketScan } from "@/lib/dashboard-types"; -import { - deriveCityMarketScanView, - resolveCityMarketScanSnapshot, - writeCachedCityMarketScan, -} from "./market-scan-state"; - -export function useCityMarketScan({ - detail, - detailCityName, - enabled = true, -}: { - detail: CityDetail | null; - detailCityName: string; - enabled?: boolean; -}) { - const { - remote: marketRemote, - reset: resetMarketRemote, - run: runMarketScanQuery, - setSuccess: setMarketScanSuccess, - } = useRemoteDataQuery(); - - useEffect(() => { - const snapshot = resolveCityMarketScanSnapshot({ - detail, - detailCityName, - enabled, - }); - if (snapshot.action === "reset") { - resetMarketRemote(); - return; - } - if (snapshot.action === "success") { - setMarketScanSuccess(snapshot.payload); - if (snapshot.shouldWriteCache) { - writeCachedCityMarketScan(snapshot.cacheKey, snapshot.payload); - } - return; - } - void runMarketScanQuery({ - request: (signal) => - scanTerminalClient.getMarketScan(detailCityName, { - lite: false, - signal, - targetDate: detail?.local_date || null, - }), - showLoading: true, - onSuccess: (payload) => { - if (payload) { - writeCachedCityMarketScan(snapshot.cacheKey, payload); - } - }, - }); - }, [ - detail, - detailCityName, - enabled, - resetMarketRemote, - runMarketScanQuery, - setMarketScanSuccess, - ]); - - const { marketScan, marketStatus } = deriveCityMarketScanView({ - detailMarketScan: detail?.market_scan, - marketRemote, - }); - - return { marketRemote, marketScan, marketStatus }; -} diff --git a/frontend/components/terminal/AnalyticsPanel.tsx b/frontend/components/terminal/AnalyticsPanel.tsx deleted file mode 100644 index 835d12bc..00000000 --- a/frontend/components/terminal/AnalyticsPanel.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client"; - -import React from "react"; -import { - BarChart2, - Target, - Zap, - Info, - Activity, - ArrowUpRight, -} from "lucide-react"; -import { Badge } from "../ui/badge"; -import { Sparkline } from "./Sparkline"; -import { CityDetail } from "../../lib/types"; - -interface AnalyticsPanelProps { - data: CityDetail; - t?: any; // Optional translations -} - -export function AnalyticsPanel({ - data, - t = {}, // Default empty for now, can be expanded via context or props -}: AnalyticsPanelProps) { - const { overview, market_scan, models } = data; - - const modelEntries = Object.entries(models) - .filter(([_, v]) => v !== undefined && v !== null) - .map(([label, value]) => ({ label, value: value as number })) - .sort((a, b) => b.value - a.value); - - const signalColor = - market_scan.signal_label === "BUY YES" - ? "border-emerald-500/30 bg-emerald-500/5 text-emerald-400" - : market_scan.signal_label === "BUY NO" - ? "border-rose-500/30 bg-rose-500/5 text-rose-400" - : "border-zinc-800 bg-zinc-900/50 text-zinc-400"; - - const signalIconColor = - market_scan.signal_label === "BUY YES" - ? "text-emerald-500" - : market_scan.signal_label === "BUY NO" - ? "text-rose-500" - : "text-zinc-500"; - - return ( -
- {/* Header Stat Area */} -
-
-

- {t.marketAnalysis || "MARKET ANALYSIS"} -

- - {t.realTime || "LIVE UPLINK"} - -
- -
-
- - {t.liveMetar || "REAL-TIME METAR"} - -
- - {overview.current_temp?.toFixed(1) || "--"} - - - {overview.temp_symbol} - -
-
-
- - {t.ensembleDeb || "ENSEMBLE DEB"} - -
- - {overview.deb_prediction?.toFixed(1) || "--"} - - - {overview.temp_symbol} - -
-
-
- - {/* Trade Execution Signal */} -
-
-
- - {t.signalStrategy || "SIGNAL STRATEGY"} - -
- - - {market_scan.signal_label} - -
-
-
- - {t.confidence || "CONFIDENCE"} - -
- {market_scan.confidence} -
-
-
-
- 0 - ? market_scan.sparkline - : [1, 1, 1] - } - width={360} - height={35} - color={ - market_scan.signal_label === "BUY YES" - ? "#10b981" - : market_scan.signal_label === "BUY NO" - ? "#f43f5e" - : "#52525b" - } - /> -
-
-
- - {/* Probability & Market Pricing */} -
-
-
- - - {t.yieldArbitrage || "YIELD ARBITRAGE"} - -
-
-
-
- {t.probComparison || "PROB COMPARISON"} - - {t.predictionGap || "GAP"}:{" "} - {market_scan.edge_percent?.toFixed(1) || "0.0"}% - -
- {/* Visual Probability Bar */} -
-
-
-
-
- -
-
- - {t.marketPriceLabel || "MIDPOINT"} - - - ${market_scan.market_price?.toFixed(2) || "--"} - -
-
- - {t.modelProbLabel || "MODEL PROB"} - - - {((market_scan.model_probability || 0) * 100).toFixed(1)}% - -
-
-
-
- -
-
- - - {t.multiModelDivergence || "MULTI-MODEL DIVERGENCE"} - -
-
- {modelEntries.map((m) => ( -
-
- - {m.label} - - - {m.value.toFixed(1)}° - -
-
-
25 - ? "bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.5)]" - : m.value > 15 - ? "bg-amber-500" - : "bg-cyan-500 shadow-[0_0_8px_rgba(34,211,238,0.5)]" - }`} - style={{ width: `${(m.value / 40) * 100}%` }} - /> -
-
- ))} -
-
-
- - {/* Execute Scan Footer */} -
- -
-
- ); -} diff --git a/frontend/components/terminal/Sidebar.tsx b/frontend/components/terminal/Sidebar.tsx deleted file mode 100644 index cf957758..00000000 --- a/frontend/components/terminal/Sidebar.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from "react"; -import { Search, Globe, ChevronRight, Activity } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; -import { CityInfo } from "@/lib/types"; -import { Sparkline } from "./Sparkline"; - -interface CitySidebarProps { - cities: CityInfo[]; - activeCity: string; - onSelectCity: (name: string) => void; - t: any; -} - -export function CitySidebar({ - cities, - activeCity, - onSelectCity, - t, -}: CitySidebarProps) { - return ( - - ); -} diff --git a/frontend/components/terminal/TerminalDashboard.tsx b/frontend/components/terminal/TerminalDashboard.tsx deleted file mode 100644 index 80a6538f..00000000 --- a/frontend/components/terminal/TerminalDashboard.tsx +++ /dev/null @@ -1,307 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { CitySidebar } from "./Sidebar"; -import { AnalyticsPanel } from "./AnalyticsPanel"; -import { - RefreshCw, - LayoutGrid, - Maximize2, - Layers, - Languages, - Sun, - Moon, -} from "lucide-react"; -import { Badge } from "@/components/ui/badge"; - -interface TerminalDashboardProps { - cities: any[]; - activeCity: string; - onSelectCity: (name: string) => void; - cityData: any; - marketData: any; - officialData: any; - isLoading: boolean; - refresh: () => void; - lang: any; - setLang: (lang: any) => void; - theme: string; - toggleTheme: () => void; - t: any; - children: React.ReactNode; // For the Map -} - -function NavLink({ - href, - label, - active = false, -}: { - href: string; - label: string; - active?: boolean; -}) { - return ( - - {label} - - ); -} - -export function TerminalDashboard({ - cities, - activeCity, - onSelectCity, - cityData, - marketData, - officialData, - isLoading, - refresh, - lang, - setLang, - theme, - toggleTheme, - t, - children, -}: TerminalDashboardProps) { - const currentTemp = cityData?.current?.temp ?? null; - const debPrediction = cityData?.deb?.prediction ?? null; - const marketPrice = marketData?.markets?.[0]?.price ?? 0.62; // Placeholder - - const modelEntries = Object.entries(cityData?.multi_model ?? {}) - .map(([label, value]) => ({ label, value: value as number })) - .filter((e) => typeof e.value === "number"); - - return ( -
- {/* Top Global Ticker / Nav */} -
-
- - - -
- -
-
- - {t.node || "Node"}: - - - CLOUD-ALPHA-01 - -
- - -
- -
-
- - -
- - - NODE: CLOUD-ALPHA-01 - - -
-
- - {/* Main Container */} -
- {/* Left: Component List */} - - - {/* Center: Interactive Map Area */} -
- {/* Map UI Overlay */} -
-
-
- - Current Focus - - - {activeCity} - -
-
-
- -
- - -
- - {/* The Map */} -
- {children} -
- - {/* Bottom Map Info Bar */} -
-
- - Projection: Mercator · Layer: Dynamic Thermal Ensemble - -
-
- - STABLE VIBE - -
-
-
- - {/* Right: Intelligence Panel */} - -
- - {/* Footer Ticker */} -
-
- - TICKER: - -
- {cities.slice(0, 10).map((c) => ( -
- - {c.display_name} - - - 24.5° - - LTAC -
- ))} -
-
-
-
- ); -} diff --git a/frontend/lib/polymarket-market-links.ts b/frontend/lib/polymarket-market-links.ts deleted file mode 100644 index e3a2816c..00000000 --- a/frontend/lib/polymarket-market-links.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { CityDetail } from "@/lib/dashboard-types"; -import type { Locale } from "@/lib/i18n"; - -const CITY_TO_MARKET_SLUG: Record = { - ankara: "ankara", - atlanta: "atlanta", - austin: "austin", - beijing: "beijing", - "buenos aires": "buenos-aires", - chengdu: "chengdu", - chicago: "chicago", - chongqing: "chongqing", - dallas: "dallas", - houston: "houston", - "hong kong": "hong-kong", - istanbul: "istanbul", - london: "london", - "los angeles": "los-angeles", - lucknow: "lucknow", - madrid: "madrid", - mexico: "mexico-city", - "mexico city": "mexico-city", - miami: "miami", - milan: "milan", - munich: "munich", - "new york": "nyc", - paris: "paris", - qingdao: "qingdao", - "san francisco": "san-francisco", - "sao paulo": "sao-paulo", - seattle: "seattle", - seoul: "seoul", - shanghai: "shanghai", - shenzhen: "shenzhen", - singapore: "singapore", - taipei: "taipei", - "tel aviv": "tel-aviv", - tokyo: "tokyo", - toronto: "toronto", - warsaw: "warsaw", - wellington: "wellington", - wuhan: "wuhan", -}; - -const MONTHS = [ - "january", - "february", - "march", - "april", - "may", - "june", - "july", - "august", - "september", - "october", - "november", - "december", -]; - -function normalizeCityKey(detail?: CityDetail | null) { - return String(detail?.name || detail?.display_name || "") - .trim() - .toLowerCase(); -} - -function slugifyCityName(cityKey: string) { - return cityKey - .trim() - .toLowerCase() - .replace(/['’.]/g, "") - .replace(/&/g, " and ") - .replace(/\s+/g, "-"); -} - -function normalizeDateParts(localDate?: string | null) { - const value = String(localDate || "").trim(); - const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); - if (!match) return null; - const year = Number(match[1]); - const monthIndex = Number(match[2]) - 1; - const day = Number(match[3]); - if ( - !Number.isFinite(year) || - !Number.isFinite(monthIndex) || - !Number.isFinite(day) || - monthIndex < 0 || - monthIndex > 11 - ) { - return null; - } - return { - year, - month: MONTHS[monthIndex], - day, - }; -} - -export function getTodayPolymarketUrl( - detail?: CityDetail | null, - locale: Locale = "en-US", -) { - const directMarketUrl = String( - detail?.market_scan?.market_url || - detail?.market_scan?.primary_market_url || - detail?.market_scan?.primary_market?.market_url || - "", - ).trim(); - if (directMarketUrl) { - return directMarketUrl; - } - - const cityKey = normalizeCityKey(detail); - const citySlug = CITY_TO_MARKET_SLUG[cityKey] || slugifyCityName(cityKey); - const dateParts = normalizeDateParts(detail?.local_date); - if (!citySlug || !dateParts) return null; - - const prefix = - locale === "zh-CN" - ? "https://polymarket.com/zh/event/" - : "https://polymarket.com/event/"; - - return `${prefix}highest-temperature-in-${citySlug}-on-${dateParts.month}-${dateParts.day}-${dateParts.year}`; -} diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts deleted file mode 100644 index 1648cb41..00000000 --- a/frontend/lib/types.ts +++ /dev/null @@ -1,420 +0,0 @@ -// --- Polymarket Market Types --- - -export interface MarketBook { - best_bid: number; - best_ask: number; - bid_levels: [number, number][]; // [price, size] - ask_levels: [number, number][]; // [price, size] -} - -export interface MarketToken { - outcome: string; - token_id: string; - implied_probability: number; - buy_price: number; - sell_price: number; - midpoint: number; - last_trade_price: number; - book?: MarketBook; -} - -export interface Trade { - id: string; - price: number; - size: number; - side: "buy" | "sell"; - timestamp: string; - timestamp_iso?: string | null; - outcome?: string | null; - asset?: string | null; - transaction_hash?: string | null; -} - -export interface MarketSnapshot { - id: string; - question: string; - title: string; - slug: string; - event_slug: string; - condition_id: string; - target_date: string; - active: boolean; - closed: boolean; - archived: boolean; - enable_order_book: boolean; - liquidity: number; - volume: number; - start_date: string; - end_date: string; - tokens: MarketToken[]; - recent_trades: Trade[]; -} - -export interface MarketPlatformData { - city: string; - target_date: string; - fetched_at: string; - market_count: number; - markets: MarketSnapshot[]; - websocket: { - market_url: string; - asset_ids: string[]; - condition_ids: string[]; - }; -} - -// --- Official Weather Types (Aviation) --- - -export interface METARObservation { - source: string; - icao: string; - station_name: string; - timestamp: string; - observation_time: string; - current: { - temp: number; - max_temp_so_far: number; - max_temp_time: string; - dewpoint: number; - wind_speed_kt: number; - wind_dir: number; - visibility_mi: string | number; - wx_desc: string | null; - altimeter: number; - clouds: any[]; - }; - recent_temps: number[]; - today_obs: any[]; - recent_obs: any[]; - unit: "celsius" | "fahrenheit"; -} - -export interface AviationWeatherData { - available: boolean; - source: string; - icao: string; - observation: METARObservation; -} - -// --- Official Weather Types (Weather.gov) --- - -export interface WeatherGovPeriod { - name: string; - start_time?: string; - end_time?: string; - is_daytime?: boolean; - temperature: number; - temperature_unit: string; - wind_speed?: string; - wind_direction?: string; - short_forecast?: string; - detailed_forecast?: string; -} - -export interface WeatherGovAlert { - id: string; - event: string; - severity: string; - certainty: string; - urgency: string; - headline: string; - onset: string; - expires: string; -} - -export interface WeatherGovData { - available: boolean; - source: string; - city: string; - grid: any; - nearest_station: { - station_identifier?: string; - name?: string; - timezone?: string; - elevation_m?: number | null; - } | null; - latest_observation: { - station_identifier?: string; - temperature_c?: number | null; - dewpoint_c?: number | null; - wind_direction_deg?: number | null; - wind_speed_kmh?: number | null; - text_description?: string | null; - timestamp?: string; - } | null; - stations: any[]; - forecast_periods: WeatherGovPeriod[]; - hourly_periods: WeatherGovPeriod[]; - active_alerts: WeatherGovAlert[]; -} - -export interface OfficialWeatherData { - city: string; - fetched_at: string; - aviation_weather?: AviationWeatherData; - weather_gov?: WeatherGovData; -} - -// --- City List Types --- - -export interface CityInfo { - name: string; - display_name: string; - lat: number; - lon: number; - risk_level: "low" | "medium" | "high"; - risk_emoji: string; - airport: string; - icao: string; - temp_unit: "celsius" | "fahrenheit"; - is_major: boolean; - settlement_source?: string; - settlement_source_label?: string; -} - -export interface CitiesResponse { - cities: CityInfo[]; -} - -// --- Full City Analysis Types --- - -export interface ModelComparison { - [key: string]: number | undefined; - "Open-Meteo"?: number; - ECMWF?: number; - GFS?: number; - ICON?: number; - GEM?: number; - JMA?: number; - LGBM?: number; - MGM?: number; - NWS?: number; -} - -export interface DEBAnalysis { - prediction: number | null; - weights_info?: string; -} - -export interface TrendInfo { - direction: string; - recent: Array<{ time: string; temp: number }>; - is_cooling: boolean; - is_dead_market: boolean; -} - -export interface PeakInfo { - hours: string[]; - first_h: number; - last_h: number; - status: string; -} - -export interface CityAnalysis { - name: string; - display_name: string; - lat: number; - lon: number; - temp_symbol: string; - local_time: string; - local_date: string; - risk: { - level: string; - emoji: string; - airport: string; - icao: string; - distance_km: number; - warning: string; - }; - current: { - temp: number | null; - max_so_far: number | null; - max_temp_time: string; - wu_settlement: number | null; - settlement_source?: string | null; - settlement_source_label?: string | null; - obs_time: string; - obs_age_min: number | null; - wind_speed_kt: number | null; - wind_dir: number | null; - humidity: number | null; - cloud_desc: string; - clouds_raw: Array<{ cover: string; base: number | null }>; - visibility_mi: number | null; - wx_desc: string | null; - raw_metar?: string | null; - report_time?: string | null; - receipt_time?: string | null; - obs_time_epoch?: number | null; - }; - mgm?: { - temp?: number | null; - time?: string; - today_high?: number | null; - today_low?: number | null; - }; - forecast: { - today_high: number | null; - daily: any[]; - }; - multi_model: ModelComparison; - deb: DEBAnalysis; - probabilities: { - mu?: number | null; - distribution?: any[]; - }; - hourly?: any; - metar_recent_obs?: any[]; - trend?: TrendInfo; - peak?: PeakInfo; - ai_analysis: string; - updated_at: string; -} - -// --- Aggregated Detail Types --- - -export interface MarketScan { - available: boolean; - reason: string | null; - primary_market: any | null; - selected_date: string | null; - selected_condition_id: string | null; - selected_slug: string | null; - temperature_bucket: any | null; - model_probability: number | null; - market_price: number | null; - midpoint?: number | null; - spread?: number | null; - edge_percent: number | null; - signal_label: "BUY YES" | "BUY NO" | "MONITOR"; - confidence: "low" | "medium" | "high"; - probability_engine?: string | null; - probability_calibration_mode?: string | null; - yes_token: MarketToken | null; - no_token: MarketToken | null; - yes_buy: number | null; - yes_sell: number | null; - yes_midpoint?: number | null; - yes_spread?: number | null; - no_buy: number | null; - no_sell: number | null; - no_midpoint?: number | null; - no_spread?: number | null; - last_trade_price: number | null; - liquidity: number | null; - volume: number | null; - quote_source?: string | null; - quote_age_ms?: number | null; - sparkline: number[]; - top_buckets?: Array<{ - label?: string | null; - value?: number | null; - temp?: number | null; - probability?: number | null; - market_price?: number | null; - yes_buy?: number | null; - yes_sell?: number | null; - no_buy?: number | null; - no_sell?: number | null; - slug?: string | null; - question?: string | null; - is_primary?: boolean; - }>; - recent_trades: Trade[]; - scan_scope?: "lite" | "full" | string | null; - websocket: any; -} - -export interface CityDetail { - city: string; - fetched_at: string; - overview: { - name: string; - display_name: string; - icao: string; - airport: string; - lat: number; - lon: number; - local_time: string; - local_date: string; - temp_symbol: string; - current_temp: number | null; - settlement_station?: { - provider_code?: string | null; - settlement_source?: string | null; - settlement_station_code?: string | null; - settlement_station_label?: string | null; - airport_code?: string | null; - airport_name?: string | null; - is_airport_anchor?: boolean; - is_official_station_anchor?: boolean; - }; - deb_prediction: number | null; - risk_level: string; - risk_warning: string; - updated_at: string; - }; - official: { - available: boolean; - metar: any; - weather_gov: WeatherGovData; - mgm: any; - mgm_nearby: any[]; - nearby_source?: string; - airport_primary?: any; - airport_primary_today_obs?: any[]; - official_nearby?: any[]; - official_network_source?: string; - official_network_status?: any; - network_lead_signal?: any; - network_spread_signal?: any; - center_station_candidate?: any; - airport_vs_network_delta?: number | null; - }; - timeseries: { - metar_recent_obs: any[]; - metar_today_obs: any[]; - settlement_today_obs?: any[]; - hourly: any; - mgm_hourly: any[]; - forecast_daily: any[]; - }; - models: ModelComparison; - probabilities: { - mu: number | null; - distribution: any[]; - }; - market_scan: MarketScan; - risk: any; - settlement_station?: any; - airport_primary?: any; - official_nearby?: any[]; - official_network_source?: string; - official_network_status?: any; - network_lead_signal?: any; - network_spread_signal?: any; - center_station_candidate?: any; - airport_vs_network_delta?: number | null; - ai_analysis: string; - errors: Record; -} - -export interface CitySummary { - name: string; - display_name: string; - icao: string; - local_time: string; - temp_symbol: string; - current: { - temp: number | null; - obs_time: string; - }; - deb: { - prediction: number | null; - }; - risk: { - level: string; - warning: string; - }; - updated_at: string; -}