diff --git a/frontend/components/dashboard/FutureForecastModalChart.tsx b/frontend/components/dashboard/FutureForecastModalChart.tsx
deleted file mode 100644
index 59aae99d..00000000
--- a/frontend/components/dashboard/FutureForecastModalChart.tsx
+++ /dev/null
@@ -1,416 +0,0 @@
-"use client";
-
-import type { ChartConfiguration } from "chart.js";
-import { useMemo } from "react";
-import { useChart } from "@/hooks/useChart";
-import { useDashboardSelection } from "@/hooks/useDashboardStore";
-import { useI18n } from "@/hooks/useI18n";
-import { getTemperatureChartData } from "@/lib/chart-utils";
-import { getFutureModalView } from "@/lib/dashboard-utils";
-import { formatMinuteAxisLabel } from "./FutureForecastModal.utils";
-
-export function DailyTemperatureChart({
- dateStr,
- forceToday = false,
-}: {
- dateStr: string;
- forceToday?: boolean;
-}) {
- const { selectedDetail: detail } = useDashboardSelection();
- const { locale, t } = useI18n();
- const view = detail ? getFutureModalView(detail, dateStr, locale) : null;
- const isToday =
- forceToday || (detail ? dateStr === detail.local_date : false);
- const todayChartData = useMemo(
- () => (detail && isToday ? getTemperatureChartData(detail, locale) : null),
- [detail, isToday, locale],
- );
-
- const canvasRef = useChart(() => {
- if (!detail || !view) {
- return {
- data: { datasets: [], labels: [] },
- type: "line",
- } satisfies ChartConfiguration<"line">;
- }
-
- if (isToday && todayChartData) {
- const datasets: NonNullable<
- ChartConfiguration<"line">["data"]
- >["datasets"] = [];
-
- datasets.push({
- borderColor: "rgba(100, 116, 139, 0.72)",
- borderDash: [6, 4],
- borderWidth: 1.6,
- data: todayChartData.datasets.debSeries,
- fill: false,
- label: locale === "en-US" ? "DEB baseline" : "DEB 原始路径",
- parsing: false,
- pointRadius: 0,
- tension: 0.1,
- });
-
- if (todayChartData.datasets.calibratedFutureSeries.length > 0) {
- datasets.push({
- borderColor: "#38bdf8",
- borderWidth: 2.4,
- data: todayChartData.datasets.calibratedFutureSeries,
- fill: false,
- label:
- locale === "en-US"
- ? "METAR-calibrated path"
- : "METAR 修正路径",
- parsing: false,
- pointHoverRadius: 5,
- pointRadius: 0,
- tension: 0.12,
- });
- }
-
- if (todayChartData.datasets.hasMgmHourly) {
- datasets.push({
- backgroundColor: "rgba(234, 179, 8, 0.05)",
- borderColor: "rgba(234, 179, 8, 0.8)",
- borderDash: [3, 3],
- borderWidth: 1.2,
- data: todayChartData.datasets.mgmHourlySeries,
- fill: false,
- label: locale === "en-US" ? "MGM Forecast" : "MGM 预测",
- parsing: false,
- pointHoverRadius: 6,
- pointRadius: 0,
- spanGaps: true,
- tension: 0.1,
- });
- }
-
- datasets.push({
- backgroundColor: "#22c55e",
- borderColor: "#22c55e",
- borderWidth: 0,
- data: todayChartData.datasets.metarSeries,
- fill: false,
- label:
- todayChartData.observationLabel ||
- (locale === "en-US" ? "Observation" : "观测实况"),
- order: 0,
- parsing: false,
- pointHoverRadius: 7,
- pointRadius: 5,
- showLine: false,
- });
-
- if (todayChartData.datasets.airportMetarSeries?.length > 0) {
- datasets.push({
- backgroundColor: "#86efac",
- borderColor: "#86efac",
- borderWidth: 1,
- data: todayChartData.datasets.airportMetarSeries,
- fill: false,
- label: locale === "en-US" ? "Airport METAR" : "机场 METAR",
- order: 0,
- parsing: false,
- pointHoverRadius: 6,
- pointRadius: 4,
- showLine: false,
- });
- }
-
- if (todayChartData.datasets.mgmSeries?.length > 0) {
- datasets.push({
- backgroundColor: "#facc15",
- borderColor: "#facc15",
- borderWidth: 0,
- data: todayChartData.datasets.mgmSeries,
- fill: false,
- label: locale === "en-US" ? "MGM Observation" : "MGM 实测",
- order: -1,
- parsing: false,
- pointHoverRadius: 9,
- pointRadius: 7,
- showLine: false,
- });
- }
-
- if (
- !todayChartData.datasets.hasMgmHourly &&
- Math.abs(todayChartData.datasets.offset) > 0.3
- ) {
- datasets.push({
- borderColor: "rgba(77, 163, 255, 0.22)",
- borderDash: [2, 4],
- borderWidth: 1,
- data: todayChartData.datasets.tempsSeries,
- fill: false,
- label: locale === "en-US" ? "OM Raw" : "OM 原始",
- parsing: false,
- pointRadius: 0,
- tension: 0.1,
- });
- }
- if ((todayChartData.tafMarkers || []).length > 0) {
- datasets.push({
- backgroundColor: "#f59e0b",
- borderColor: "#f59e0b",
- borderWidth: 0,
- data: todayChartData.datasets.tafCurrentMarkerSeries,
- fill: false,
- label: locale === "en-US" ? "Current TAF" : "当前 TAF",
- order: -3,
- parsing: false,
- pointHoverRadius: 8,
- pointRadius: 6,
- showLine: false,
- });
- datasets.push({
- backgroundColor: "rgba(250, 204, 21, 0.72)",
- borderColor: "rgba(250, 204, 21, 0.72)",
- borderWidth: 0,
- data: todayChartData.datasets.tafPeakWindowMarkerSeries,
- fill: false,
- label: locale === "en-US" ? "Peak-window TAF" : "峰值窗口 TAF",
- order: -2,
- parsing: false,
- pointHoverRadius: 7,
- pointRadius: 4,
- showLine: false,
- });
- datasets.push({
- backgroundColor: "#f59e0b",
- borderColor: "#f59e0b",
- borderWidth: 0,
- data: todayChartData.datasets.tafMarkerSeries,
- fill: false,
- label: locale === "en-US" ? "TAF Timing" : "TAF 时段",
- order: -4,
- parsing: false,
- pointHoverRadius: 0,
- pointRadius: 0,
- showLine: false,
- });
- }
-
- return {
- data: {
- datasets,
- labels: [],
- },
- options: {
- interaction: { intersect: false, mode: "nearest" },
- maintainAspectRatio: false,
- plugins: {
- legend: {
- labels: {
- color: "#9FB2C7",
- filter: (legendItem, chartData) => {
- const text = String(legendItem.text || "");
- if (!text) return false;
- if (text === "TAF Timing" || text === "TAF 时段")
- return false;
- if (!text.includes("DEB")) return true;
-
- const firstDebIndex = (chartData.datasets || []).findIndex(
- (dataset) => String(dataset.label || "").includes("DEB"),
- );
- return legendItem.datasetIndex === firstDebIndex;
- },
- font: { family: "Inter", size: 11 },
- },
- },
- tooltip: {
- backgroundColor: "rgba(15, 23, 42, 0.96)",
- borderColor: "rgba(77, 163, 255, 0.24)",
- borderWidth: 1,
- callbacks: {
- title: (items) => {
- const rawX = items?.[0]?.parsed?.x;
- return rawX != null
- ? formatMinuteAxisLabel(Number(rawX))
- : "";
- },
- label: (ctx) => {
- const label = String(ctx.dataset.label || "");
- const raw = ctx.raw as
- | {
- marker?: {
- summary?: string;
- markerType?: string;
- displayType?: string;
- isCurrent?: boolean;
- isPeakWindow?: boolean;
- };
- }
- | undefined;
- if (
- label === "TAF Timing" ||
- label === "TAF 时段" ||
- label === "Current TAF" ||
- label === "当前 TAF" ||
- label === "Peak-window TAF" ||
- label === "峰值窗口 TAF"
- ) {
- const marker = raw?.marker;
- if (!marker) return label;
- const markerType = String(marker.markerType || "");
- const displayType = String(
- marker.displayType || marker.markerType || "",
- );
- const summary = String(marker.summary || "");
- const prefix =
- marker.isCurrent && marker.isPeakWindow
- ? locale === "en-US"
- ? "Current / peak-window TAF"
- : "当前 / 峰值窗口 TAF"
- : marker.isCurrent
- ? locale === "en-US"
- ? "Current TAF"
- : "当前 TAF"
- : marker.isPeakWindow
- ? locale === "en-US"
- ? "Peak-window TAF"
- : "峰值窗口 TAF"
- : label;
- return `${prefix}: ${
- markerType
- ? summary.replace(markerType, displayType)
- : summary
- }`;
- }
- const value = ctx.parsed.y;
- if (value == null) return label;
- return `${label}: ${value.toFixed(1)}${detail.temp_symbol || "°C"}`;
- },
- },
- },
- },
- responsive: true,
- scales: {
- x: {
- max: todayChartData.xMax,
- min: todayChartData.xMin,
- grid: { color: "rgba(255,255,255,0.04)" },
- type: "linear",
- ticks: {
- callback: (value) => {
- const num = Number(value);
- if (!Number.isFinite(num)) return "";
- const minutes = Math.round(num);
- if (
- minutes !== todayChartData.xMin &&
- minutes !== todayChartData.xMax &&
- minutes % 120 !== 0
- ) {
- return "";
- }
- return formatMinuteAxisLabel(minutes);
- },
- color: "#6B7A90",
- font: { family: "Inter", size: 10 },
- maxRotation: 0,
- },
- },
- y: {
- grid: { color: "rgba(255,255,255,0.04)" },
- max: todayChartData.max,
- min: todayChartData.min,
- ticks: {
- callback: (value) =>
- `${Number(value).toFixed(todayChartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`,
- color: "#6B7A90",
- font: { family: "Inter", size: 10 },
- stepSize: todayChartData.yTickStep,
- },
- },
- },
- },
- type: "line",
- } satisfies ChartConfiguration<"line">;
- }
-
- const labels = view.slice.map((point) => point.label);
- const unit = detail.temp_symbol || "°C";
-
- return {
- data: {
- datasets: [
- {
- backgroundColor: "rgba(77, 163, 255, 0.08)",
- borderColor: "#4DA3FF",
- data: view.slice.map((point) => point.temp),
- fill: false,
- label:
- locale === "en-US" ? "Open-Meteo Temperature" : "Open-Meteo 温度",
- pointRadius: 2,
- tension: 0.1,
- },
- {
- backgroundColor: "transparent",
- borderColor: "#93C5FD",
- borderDash: [5, 4],
- data: view.slice.map((point) => point.dewPoint),
- fill: false,
- label: locale === "en-US" ? "Dew Point" : "露点",
- pointRadius: 0,
- tension: 0.1,
- },
- ],
- labels,
- },
- options: {
- interaction: { intersect: false, mode: "index" },
- maintainAspectRatio: false,
- plugins: {
- legend: {
- labels: {
- color: "#9FB2C7",
- font: { family: "Inter", size: 11 },
- },
- },
- tooltip: {
- backgroundColor: "rgba(15, 23, 42, 0.96)",
- borderColor: "rgba(77, 163, 255, 0.24)",
- borderWidth: 1,
- callbacks: {
- label: (ctx) =>
- `${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}${unit}`,
- },
- },
- },
- responsive: true,
- scales: {
- x: {
- grid: { color: "rgba(255,255,255,0.04)" },
- ticks: {
- color: "#6B7A90",
- font: { family: "Inter", size: 10 },
- maxRotation: 0,
- },
- },
- y: {
- grid: { color: "rgba(255,255,255,0.04)" },
- ticks: {
- callback: (value) => `${value}${unit}`,
- color: "#6B7A90",
- font: { family: "Inter", size: 10 },
- },
- },
- },
- },
- type: "line",
- } satisfies ChartConfiguration<"line">;
- }, [detail, isToday, locale, todayChartData, view]);
-
- return (
- <>
-
-
-
- {isToday && (
-
- {todayChartData?.legendText || t("future.chartLegendEmpty")}
-
- )}
- >
- );
-}
diff --git a/frontend/components/dashboard/FutureForecastModalStatus.tsx b/frontend/components/dashboard/FutureForecastModalStatus.tsx
deleted file mode 100644
index 85ff77a3..00000000
--- a/frontend/components/dashboard/FutureForecastModalStatus.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import clsx from "clsx";
-
-export type FutureSyncStatusItem = {
- key: string;
- state: "ready" | "syncing";
- label: string;
- note: string;
-};
-
-export function FutureRefreshLock({ locale }: { locale: string }) {
- return (
-
-
-
-
- {locale === "en-US"
- ? "Refreshing latest intraday data"
- : "正在刷新最新日内数据"}
-
-
- {locale === "en-US"
- ? "Old cached readings are temporarily locked to prevent misjudgement. The analysis will unlock after the latest anchor observation, model layer, and probability layer are ready."
- : "旧缓存读数已临时锁定,避免误判。最新锚点观测、模型层和概率层就绪后会自动解锁。"}
-
-
-
- );
-}
-
-export function FutureSyncStatusStrip({
- items,
- compact = false,
-}: {
- items: readonly FutureSyncStatusItem[];
- compact?: boolean;
-}) {
- return (
-
- {items.map((item) => (
-
-
-
- {item.label}
- {item.note}
-
-
- ))}
-
- );
-}
diff --git a/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx b/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx
deleted file mode 100644
index ae80b950..00000000
--- a/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import {
- Cloud,
- CloudFog,
- CloudLightning,
- CloudRain,
- CloudSnow,
- CloudSun,
- Search,
- Sun,
- Wind,
-} from "lucide-react";
-
-export function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
- if (emoji === "☀️") return ;
- if (emoji === "⛅" || emoji === "🌤️")
- return ;
- if (emoji === "☁️") return ;
- if (emoji === "🌧️" || emoji === "🌦️")
- return ;
- if (emoji === "⛈️") return ;
- if (emoji === "❄️" || emoji === "🌨️")
- return ;
- if (emoji === "🌫️") return ;
- if (emoji === "💨") return ;
- return ;
-}
diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx
deleted file mode 100644
index 692a648b..00000000
--- a/frontend/components/dashboard/HeaderBar.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import Image from "next/image";
-import { usePathname } from "next/navigation";
-import clsx from "clsx";
-import {
- LogIn,
- UserRound,
- RotateCw,
- BookOpen,
- MoreHorizontal,
- CloudSun,
-} from "lucide-react";
-import { useEffect, useState } from "react";
-import { useDashboardStore, useProAccess } from "@/hooks/useDashboardStore";
-import { useI18n } from "@/hooks/useI18n";
-
-function parseExpiryInfo(raw?: string | null) {
- const text = String(raw || "").trim();
- if (!text) return null;
- const dt = new Date(text);
- if (Number.isNaN(dt.getTime())) return null;
- const diffMs = dt.getTime() - Date.now();
- const daysLeft = Math.ceil(diffMs / 86_400_000);
- return {
- date: dt,
- daysLeft,
- expired: diffMs <= 0,
- };
-}
-
-export function HeaderBar({
- refreshAction,
- refreshSpinning,
-}: {
- refreshAction?: () => void | Promise;
- refreshSpinning?: boolean;
-}) {
- const store = useDashboardStore();
- const { proAccess } = useProAccess();
- const { locale, t, toggleLocale } = useI18n();
- const pathname = usePathname();
- const isAuthenticated = proAccess.authenticated;
- const docsHref = "/docs/intro";
- const docsActive = pathname?.startsWith("/docs");
- const navItems = [
- {
- href: "/",
- label: locale === "en-US" ? "Dashboard" : "总览",
- active: pathname === "/",
- },
- ];
- const isRefreshing = refreshSpinning ?? store.loadingState.refresh;
- const handleRefresh = () => {
- if (refreshAction) {
- return void refreshAction();
- }
- return void store.refreshAll();
- };
-
- const accountHref = isAuthenticated
- ? "/account"
- : "/auth/login?next=%2Faccount";
- const accountAria = isAuthenticated
- ? t("header.accountAria")
- : t("header.signInAria");
- const effectiveExpiry = proAccess.subscriptionActive
- ? proAccess.subscriptionTotalExpiresAt ||
- proAccess.subscriptionExpiresAt
- : proAccess.subscriptionExpiresAt;
- const expiryInfo = parseExpiryInfo(effectiveExpiry);
- const hasQueuedExtension = Boolean(
- proAccess.subscriptionActive &&
- proAccess.subscriptionQueuedDays > 0,
- );
- const showRenewReminder =
- isAuthenticated &&
- !proAccess.loading &&
- !hasQueuedExtension &&
- ((proAccess.subscriptionActive &&
- expiryInfo &&
- expiryInfo.daysLeft <= 3) ||
- (!proAccess.subscriptionActive && Boolean(expiryInfo)));
- const renewReminderLabel = !showRenewReminder
- ? ""
- : !proAccess.subscriptionActive
- ? locale === "en-US"
- ? "Pro expired"
- : "Pro 已到期"
- : locale === "en-US"
- ? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
- : `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`;
-
-
- return (
-
- );
-}
-
diff --git a/frontend/components/dashboard/IntradaySignalScene.tsx b/frontend/components/dashboard/IntradaySignalScene.tsx
deleted file mode 100644
index 8cb16a8d..00000000
--- a/frontend/components/dashboard/IntradaySignalScene.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-"use client";
-
-import { CSSProperties, useEffect, useRef } from "react";
-import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion";
-
-export interface IntradaySignalMetric {
- key: string;
- label: string;
- value: string;
- hint: string;
- fill: number | null;
- tone: string;
-}
-
-function clamp(value: number, min: number, max: number) {
- return Math.min(Math.max(value, min), max);
-}
-
-function getToneColor(tone: string) {
- if (tone === "cyan") return "#4DA3FF";
- if (tone === "blue") return "#60a5fa";
- if (tone === "amber") return "#f59e0b";
- return "#9FB2C7";
-}
-
-function hexToRgba(hex: string, alpha: number) {
- const sanitized = hex.replace("#", "");
- const numeric = Number.parseInt(sanitized.padEnd(6, "0"), 16);
- const r = (numeric >> 16) & 255;
- const g = (numeric >> 8) & 255;
- const b = numeric & 255;
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
-}
-
-/* ── Draw a rounded-rect path (polyfill-safe) ── */
-
-function roundRect(
- ctx: CanvasRenderingContext2D,
- x: number,
- y: number,
- w: number,
- h: number,
- r: number | [number, number, number, number],
-) {
- const corners = typeof r === "number" ? [r, r, r, r] : r;
- const [tl, tr, br, bl] = corners;
- ctx.beginPath();
- ctx.moveTo(x + tl, y);
- ctx.lineTo(x + w - tr, y);
- ctx.quadraticCurveTo(x + w, y, x + w, y + tr);
- ctx.lineTo(x + w, y + h - br);
- ctx.quadraticCurveTo(x + w, y + h, x + w - br, y + h);
- ctx.lineTo(x + bl, y + h);
- ctx.quadraticCurveTo(x, y + h, x, y + h - bl);
- ctx.lineTo(x, y + tl);
- ctx.quadraticCurveTo(x, y, x + tl, y);
- ctx.closePath();
-}
-
-/* ── Component ── */
-
-export function IntradaySignalScene({
- metrics,
- score,
-}: {
- metrics: IntradaySignalMetric[];
- score: number;
-}) {
- const containerRef = useRef(null);
- const prefersReducedMotion = usePrefersReducedMotion();
-
- useEffect(() => {
- const host = containerRef.current;
- if (!host) return;
-
- const canvas = document.createElement("canvas");
- canvas.style.cssText = "width:100%;height:100%;display:block;";
- host.appendChild(canvas);
-
- const ctx = canvas.getContext("2d")!;
- let animationId = 0;
-
- const resize = () => {
- const rect = host.getBoundingClientRect();
- const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
- canvas.width = rect.width * dpr;
- canvas.height = rect.height * dpr;
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- };
-
- resize();
-
- const observer = new ResizeObserver(resize);
- observer.observe(host);
-
- let startTime = performance.now();
-
- const render = (now: number) => {
- animationId = requestAnimationFrame(render);
-
- const W = host.clientWidth;
- const H = host.clientHeight;
- if (W < 1 || H < 1) return;
-
- ctx.clearRect(0, 0, W, H);
-
- const elapsed = prefersReducedMotion ? 0 : (now - startTime) / 1000;
-
- /* ── Layout ── */
- const cx = W / 2;
- const floorY = Math.max(H - 28, H * 0.78);
- const rx = clamp(Math.min(W / 2 - 24, 160), 60, 160);
- const ry = clamp(rx * 0.28, 12, 26);
-
- const isPositive = score >= 0;
- const accentColor = isPositive ? "#4DA3FF" : "#F59E0B";
- const floorGlowColor = isPositive ? "#0e7490" : "#b45309";
- const floorBaseColor = isPositive ? "#10263b" : "#2c1d12";
-
- /* ── Floor ── */
- const floorGrad = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx);
- floorGrad.addColorStop(0, hexToRgba(floorGlowColor, 0.55));
- floorGrad.addColorStop(0.5, floorBaseColor);
- floorGrad.addColorStop(1, "#060e1a");
-
- ctx.beginPath();
- ctx.ellipse(cx, floorY, rx, ry, 0, 0, Math.PI * 2);
- ctx.fillStyle = floorGrad;
- ctx.fill();
-
- /* Floor glow overlay */
- const floorGlow = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx * 0.6);
- floorGlow.addColorStop(0, hexToRgba(accentColor, 0.08));
- floorGlow.addColorStop(1, "transparent");
- ctx.beginPath();
- ctx.ellipse(cx, floorY, rx * 0.6, ry * 0.6, 0, 0, Math.PI * 2);
- ctx.fillStyle = floorGlow;
- ctx.fill();
-
- /* ── Ring (oscillating opacity) ── */
- const ringOpacity = 0.38 + Math.sin(elapsed * 0.8) * 0.1;
- ctx.beginPath();
- ctx.ellipse(cx, floorY, rx - 10, Math.max(ry - 3, 8), 0, 0, Math.PI * 2);
- ctx.strokeStyle = hexToRgba(accentColor, ringOpacity);
- ctx.lineWidth = 2.5;
- ctx.stroke();
-
- /* ── Bar positions ── */
- const barSpacing = rx * 0.48;
- const xPositions = [
- cx - barSpacing,
- cx - barSpacing / 3,
- cx + barSpacing / 3,
- cx + barSpacing,
- ];
- const barWidth = Math.min(26, rx * 0.16);
- const limitedMetrics = metrics.slice(0, 4);
-
- /* ── Draw bars ── */
- limitedMetrics.forEach((metric, i) => {
- const fill = metric.fill ?? 20;
- const height = Math.max(12, 14 + (fill / 100) * (floorY - 55));
- const bx = xPositions[i];
- const by = floorY - height;
- const color = getToneColor(metric.tone);
- const baseGlow = prefersReducedMotion
- ? 1
- : 1 + Math.sin(elapsed * 1.2 + i) * 0.06;
-
- /* Base glow ellipse */
- const glowGrad = ctx.createRadialGradient(bx, floorY, 0, bx, floorY, barWidth * 1.2);
- glowGrad.addColorStop(0, hexToRgba(color, 0.35));
- glowGrad.addColorStop(1, "transparent");
- ctx.beginPath();
- ctx.ellipse(bx, floorY, barWidth * baseGlow, 7 * baseGlow, 0, 0, Math.PI * 2);
- ctx.fillStyle = glowGrad;
- ctx.fill();
-
- /* Bar body (rounded top) */
- const barGrad = ctx.createLinearGradient(bx, by, bx, floorY);
- barGrad.addColorStop(0, color);
- barGrad.addColorStop(0.55, color);
- barGrad.addColorStop(1, hexToRgba(color, 0.3));
- roundRect(ctx, bx - barWidth / 2, by, barWidth, height, [4, 4, 0, 0]);
- ctx.fillStyle = barGrad;
- ctx.fill();
-
- /* Bar glow outline */
- ctx.strokeStyle = hexToRgba(color, 0.15);
- ctx.lineWidth = 1;
- roundRect(ctx, bx - barWidth / 2 - 1, by - 1, barWidth + 2, height + 2, [5, 5, 0, 0]);
- ctx.stroke();
-
- /* Cap (pulse offset per bar) */
- const pulse = prefersReducedMotion
- ? 0
- : Math.sin(elapsed * 1.5 + i * 0.8) * 3;
- const capY = by + pulse;
- const capR = Math.max(5, barWidth * 0.22);
-
- /* Cap outer glow */
- const capGlow = ctx.createRadialGradient(bx, capY, 0, bx, capY, capR * 3);
- capGlow.addColorStop(0, hexToRgba(color, 0.2));
- capGlow.addColorStop(1, "transparent");
- ctx.beginPath();
- ctx.arc(bx, capY, capR * 3, 0, Math.PI * 2);
- ctx.fillStyle = capGlow;
- ctx.fill();
-
- /* Cap body */
- ctx.beginPath();
- ctx.arc(bx, capY, capR, 0, Math.PI * 2);
- ctx.fillStyle = color;
- ctx.shadowColor = color;
- ctx.shadowBlur = capR * 2;
- ctx.fill();
- ctx.shadowBlur = 0;
- });
-
- /* ── Ambient sparkle dots (only when not reduced motion) ── */
- if (!prefersReducedMotion) {
- for (let i = 0; i < 6; i++) {
- const angle = elapsed * 0.25 + i * 1.05;
- const dist = rx * 0.7 + Math.sin(elapsed * 0.15 + i * 2.3) * 10;
- const sx = cx + Math.cos(angle) * dist;
- const sy = floorY - ry * 0.4 + Math.sin(angle) * dist * 0.35;
- const sparkleSize = 1.5 + Math.sin(elapsed * 2 + i * 1.7) * 0.8;
- ctx.beginPath();
- ctx.arc(sx, sy, Math.max(0.5, sparkleSize), 0, Math.PI * 2);
- ctx.fillStyle = hexToRgba(accentColor, 0.12 + Math.sin(elapsed * 1.3 + i) * 0.06);
- ctx.fill();
- }
- }
- }
-
- animationId = requestAnimationFrame(render);
-
- return () => {
- observer.disconnect();
- cancelAnimationFrame(animationId);
- if (canvas.parentNode === host) {
- host.removeChild(canvas);
- }
- };
- }, [metrics, prefersReducedMotion, score]);
-
- return (
-
-
-
- {metrics.slice(0, 4).map((metric) => (
-
-
-
- {metric.label}
-
- {metric.value} · {metric.hint}
-
-
-
- ))}
-
-
- );
-}
diff --git a/frontend/components/dashboard/MapCanvas.tsx b/frontend/components/dashboard/MapCanvas.tsx
deleted file mode 100644
index cdf84dc2..00000000
--- a/frontend/components/dashboard/MapCanvas.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-"use client";
-
-import "leaflet/dist/leaflet.css";
-
-import { useDashboardStore } from "@/hooks/useDashboardStore";
-import { useLeafletMap } from "@/hooks/useLeafletMap";
-
-export function MapCanvas({
- onCitySelect,
- selectionMode = "focus",
-}: {
- onCitySelect?: (cityName: string) => void;
- selectionMode?: "focus" | "select";
-} = {}) {
- const store = useDashboardStore();
- const { containerRef } = useLeafletMap({
- cities: store.cities,
- cityDetailsByName: store.cityDetailsByName,
- citySummariesByName: store.citySummariesByName,
- onClosePanel: store.closePanel,
- onEnsureCityDetail: store.ensureCityDetail,
- onMapInteractionChange: store.setMapInteractionActive,
- onRegisterStopMotion: store.registerMapStopMotion,
- onSelectCity: (cityName) => {
- onCitySelect?.(cityName);
- if (selectionMode === "select") {
- void store.selectCity(cityName);
- return;
- }
- void store.focusCity(cityName);
- },
- selectedCity: store.selectedCity,
- selectedDetail: store.selectedDetail,
- suspendMotion: Boolean(store.futureModalDate),
- isLoadingDetail: store.loadingState.cityDetail,
- });
-
- return
;
-}
diff --git a/frontend/components/dashboard/PolyWeatherDashboard.tsx b/frontend/components/dashboard/PolyWeatherDashboard.tsx
deleted file mode 100644
index 54c7e0da..00000000
--- a/frontend/components/dashboard/PolyWeatherDashboard.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-"use client";
-
-import { ScanTerminalDashboard } from "@/components/dashboard/ScanTerminalDashboard";
-
-export function PolyWeatherDashboard() {
- return ;
-}
diff --git a/frontend/components/dashboard/ScanFilterPanel.tsx b/frontend/components/dashboard/ScanFilterPanel.tsx
deleted file mode 100644
index 7fc3a0d1..00000000
--- a/frontend/components/dashboard/ScanFilterPanel.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-"use client";
-
-import React from "react";
-import {
- CircleDot,
- Clock3,
- Info,
- TrendingUp,
- Zap,
-} from "lucide-react";
-import { useI18n } from "@/hooks/useI18n";
-import type { ScanTerminalFilters } from "@/lib/dashboard-types";
-
-export interface FilterState extends ScanTerminalFilters {}
-
-const SCAN_MODES = [
- {
- key: "tradable" as const,
- icon: Zap,
- labelEn: "Tradable",
- labelZh: "可交易机会",
- descEn: "Find the best immediate trade",
- descZh: "发现当前最值得交易的市场",
- },
- {
- key: "early" as const,
- icon: Clock3,
- labelEn: "Early",
- labelZh: "早期机会",
- descEn: "Long-horizon, lower-priced setups",
- descZh: "长时间布局,低价市场",
- },
- {
- key: "touch" as const,
- icon: CircleDot,
- labelEn: "Touch Play",
- labelZh: "触达博弈",
- descEn: "Markets approaching the settle line",
- descZh: "接近决策,博弈是否触达",
- },
- {
- key: "trend" as const,
- icon: TrendingUp,
- labelEn: "Trend",
- labelZh: "趋势确认",
- descEn: "Trend-confirmed follow-through",
- descZh: "趋势明朗,顺势交易",
- },
-] as const;
-
-export function ScanFilterPanel({
- value,
- onChange,
-}: {
- value: FilterState;
- onChange?: (filters: FilterState) => void;
-}) {
- const { locale } = useI18n();
- const isEn = locale === "en-US";
-
- const updateFilter = (
- key: K,
- nextValue: FilterState[K],
- ) => {
- onChange?.({
- ...value,
- [key]: nextValue,
- });
- };
-
- return (
-
-
-
-
-
- {isEn ? "Scan Mode" : "扫描模式"}
-
-
-
- {SCAN_MODES.map((mode) => {
- const Icon = mode.icon;
- const isActive = value.scan_mode === mode.key;
- return (
- updateFilter("scan_mode", mode.key)}
- >
-
-
-
-
-
- {isEn ? mode.labelEn : mode.labelZh}
-
-
- {isEn ? mode.descEn : mode.descZh}
-
-
-
- );
- })}
-
-
-
-
- );
-}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx
index 75898c90..ca528d38 100644
--- a/frontend/components/dashboard/ScanTerminalDashboard.tsx
+++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx
@@ -394,15 +394,11 @@ function CityGroupedTable({
{isExpanded && buckets.map((row) => (
onSelect(row)}
+ className="border-b border-slate-50 bg-slate-50/30 text-slate-500"
>
- {row.target_label || "--"}
+ {row.target_label || "--"}
{pct(row.edge_percent)}
diff --git a/frontend/components/dashboard/WeatherAuraLayer.tsx b/frontend/components/dashboard/WeatherAuraLayer.tsx
deleted file mode 100644
index a5c116c9..00000000
--- a/frontend/components/dashboard/WeatherAuraLayer.tsx
+++ /dev/null
@@ -1,451 +0,0 @@
-"use client";
-
-import { CSSProperties, useEffect, useMemo, useState } from "react";
-import { useDashboardSelection } from "@/hooks/useDashboardStore";
-import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion";
-import { getWeatherAuraProfile } from "@/lib/weather-aura";
-
-/* ──────────────────────────────────────────────────────────────
- Pure-CSS weather-aura particle layer
- Replaces the former Three.js WebGL implementation with
- CSS @keyframes animations — no canvas, no WebGL.
- ────────────────────────────────────────────────────────────── */
-
-function hexToRgba(hex: string, alpha: number) {
- const sanitized = hex.replace("#", "");
- const normalized =
- sanitized.length === 3
- ? sanitized
- .split("")
- .map((char) => `${char}${char}`)
- .join("")
- : sanitized.padEnd(6, "0");
- const numeric = Number.parseInt(normalized, 16);
- const r = (numeric >> 16) & 255;
- const g = (numeric >> 8) & 255;
- const b = numeric & 255;
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
-}
-
-/* ── Injection-safe style IDs ── */
-
-const AURA_STYLE_ID = "poly-aura-keyframes";
-
-/* ── CSS @keyframes (injected once via