From 20a3d399d9f972df499b43310d73400572bb9b9c Mon Sep 17 00:00:00 2001 From: shangzy Date: Tue, 7 Apr 2026 15:19:10 +0800 Subject: [PATCH] fix: resolve all eslint errors (set-state-in-effect, nested components, no-explicit-any) - Wrap synchronous setState calls in useEffect with startTransition to avoid cascading renders - Convert nested SortIcon components to renderSortIcon helper functions - Replace all `any` types with proper interfaces (OverviewData, TrendPoint, RankItem) - Remove unused formatTokens import in logs page - Add no-any rule to CLAUDE.md --- CLAUDE.md | 2 ++ app/aggregation/page.tsx | 18 ++++++++-------- app/detail/[...slug]/page.tsx | 8 +++---- app/logs/page.tsx | 10 ++++----- app/page.tsx | 39 ++++++++++++++++++++++++++++------- app/rankings/page.tsx | 14 ++++++------- lib/i18n.tsx | 6 ++++-- 7 files changed, 62 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e81620e..5199fd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,8 @@ Docker deployment: port 8019, `docker-compose up` with `.env.production`, extern Next.js 16 App Router analytics dashboard for an API gateway. React 19, TypeScript strict mode, Tailwind CSS 4, Recharts 3, PostgreSQL. +**TypeScript: Never use `any` type.** Always define proper interfaces/types for all data structures. Use `unknown` with type narrowing if the type is truly unknown. + ### Three Global Contexts (wrapped in `components/ClientProviders.tsx`) 1. **ThemeProvider** (`lib/theme.tsx`) — light/dark/system, localStorage `theme`, supports iframe embedding via `?theme=` query param and postMessage sync diff --git a/app/aggregation/page.tsx b/app/aggregation/page.tsx index db033e6..5e94570 100644 --- a/app/aggregation/page.tsx +++ b/app/aggregation/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback, useRef } from "react"; +import { useEffect, useState, useCallback, useRef, startTransition } from "react"; import { createPortal } from "react-dom"; import { motion } from "motion/react"; import { Users, Calendar, Hash, Zap, ArrowUpDown, ArrowDown, ArrowUp, HelpCircle } from "lucide-react"; @@ -22,7 +22,7 @@ function RatioTooltip({ text }: { text: string }) { const iconRef = useRef(null); const [mounted, setMounted] = useState(false); - useEffect(() => { setMounted(true); }, []); + useEffect(() => { startTransition(() => setMounted(true)); }, []); function handleEnter() { if (iconRef.current) { @@ -84,11 +84,11 @@ export default function AggregationPage() { const [sortAsc, setSortAsc] = useState(false); const fetchData = useCallback(async () => { - setLoading(true); + startTransition(() => setLoading(true)); const { start, end } = getEffectiveRange(); const res = await fetch(buildQuery("/api/aggregation", { start, end })); - setData(await res.json()); - setLoading(false); + const json = await res.json(); + startTransition(() => { setData(json); setLoading(false); }); }, [getEffectiveRange]); useEffect(() => { fetchData(); }, [fetchData]); @@ -112,12 +112,12 @@ export default function AggregationPage() { else { setSortKey(key); setSortAsc(false); } } - function SortIcon({ col }: { col: SortKey }) { + const renderSortIcon = (col: SortKey) => { if (sortKey !== col) return ; return sortAsc ? : ; - } + }; const sortHeaders: { key: SortKey; label: string }[] = [ { key: "calls", label: t("th.calls") }, @@ -165,14 +165,14 @@ export default function AggregationPage() { {sortHeaders.map(h => ( handleSort(h.key)}> - {h.label} + {h.label} {renderSortIcon(h.key)} ))} handleSort("ratio")}> {t("agg.ratio")} - + {renderSortIcon("ratio")} diff --git a/app/detail/[...slug]/page.tsx b/app/detail/[...slug]/page.tsx index 624711a..7f77f17 100644 --- a/app/detail/[...slug]/page.tsx +++ b/app/detail/[...slug]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, startTransition } from "react"; import { useParams } from "next/navigation"; import { motion } from "motion/react"; import { ArrowLeft, Hash, Zap, MessageSquare } from "lucide-react"; @@ -30,11 +30,11 @@ export default function DetailPage() { const { getEffectiveRange } = useTimeRange(); const [data, setData] = useState(null); - const [trends, setTrends] = useState([]); + const [trends, setTrends] = useState<{ date: string; calls: number; total_tokens: number; prompt_tokens: number; completion_tokens: number }[]>([]); const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { - setLoading(true); + startTransition(() => setLoading(true)); const { start, end } = getEffectiveRange(); const tp = { start, end }; const [detail, tr] = await Promise.all([ @@ -45,7 +45,7 @@ export default function DetailPage() { ...(type === "channel" ? { channel_id: decodedId } : {}), })).then(r => r.json()), ]); - setData(detail); setTrends(tr); setLoading(false); + startTransition(() => { setData(detail); setTrends(tr); setLoading(false); }); }, [type, decodedId, getEffectiveRange]); useEffect(() => { fetchData(); }, [fetchData]); diff --git a/app/logs/page.tsx b/app/logs/page.tsx index 8bedd7a..bc19bc0 100644 --- a/app/logs/page.tsx +++ b/app/logs/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, startTransition } from "react"; import { motion } from "motion/react"; import { ScrollText, Search, ChevronLeft, ChevronRight, Zap } from "lucide-react"; import { TimeRangeSelector } from "@/components/TimeRangeSelector"; -import { buildQuery, formatNumber, formatTokens, formatDate } from "@/lib/utils"; +import { buildQuery, formatNumber, formatDate } from "@/lib/utils"; import { useTimeRange } from "@/lib/time-range-context"; import { useI18n } from "@/lib/i18n"; @@ -26,15 +26,15 @@ export default function LogsPage() { const pageSize = 100; const fetchData = useCallback(async () => { - setLoading(true); + startTransition(() => setLoading(true)); const { start, end } = getEffectiveRange(); const res = await fetch(buildQuery("/api/logs", { start, end, page, page_size: pageSize, ...filters })); const data = await res.json(); - setLogs(data.logs); setTotal(data.total); setLoading(false); + startTransition(() => { setLogs(data.logs); setTotal(data.total); setLoading(false); }); }, [page, filters, getEffectiveRange]); useEffect(() => { fetchData(); }, [fetchData]); - useEffect(() => { setPage(1); }, [getEffectiveRange, filters]); + useEffect(() => { startTransition(() => setPage(1)); }, [getEffectiveRange, filters]); const totalPages = Math.ceil(total / pageSize); const headers = [t("th.time"), t("th.user"), t("th.realModel"), t("th.channel"), t("th.input"), t("th.output"), t("th.totalToken"), t("th.latency"), ""]; diff --git a/app/page.tsx b/app/page.tsx index b77c1ff..27bd4f1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, startTransition } from "react"; import { motion } from "motion/react"; import { Zap, Hash, Users, Cpu, TrendingUp, BarChart3 } from "lucide-react"; import { StatsCard } from "@/components/StatsCard"; @@ -11,19 +11,40 @@ import { buildQuery } from "@/lib/utils"; import { useTimeRange } from "@/lib/time-range-context"; import { useI18n } from "@/lib/i18n"; +interface OverviewData { + total_calls: number; + total_tokens: number; + active_users: number; + active_models: number; +} + +interface TrendPoint { + date: string; + calls: number; + total_tokens: number; + prompt_tokens: number; + completion_tokens: number; +} + +interface RankItem { + name: string; + total_tokens: number; + calls: number; +} + export default function DashboardPage() { const { t } = useI18n(); const { getEffectiveRange } = useTimeRange(); const [granularity, setGranularity] = useState<"day" | "week" | "month">("day"); const [trendMetric, setTrendMetric] = useState<"total_tokens" | "calls">("total_tokens"); - const [overview, setOverview] = useState(null); - const [trends, setTrends] = useState([]); - const [userRank, setUserRank] = useState([]); - const [modelRank, setModelRank] = useState([]); + const [overview, setOverview] = useState(null); + const [trends, setTrends] = useState([]); + const [userRank, setUserRank] = useState([]); + const [modelRank, setModelRank] = useState([]); const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { - setLoading(true); + startTransition(() => setLoading(true)); const { start, end } = getEffectiveRange(); const tp = { start, end }; const [ov, tr, ur, mr] = await Promise.all([ @@ -32,7 +53,9 @@ export default function DashboardPage() { fetch(buildQuery("/api/rankings", { ...tp, type: "user", limit: 10 })).then(r => r.json()), fetch(buildQuery("/api/rankings", { ...tp, type: "model", limit: 10 })).then(r => r.json()), ]); - setOverview(ov); setTrends(tr); setUserRank(ur); setModelRank(mr); setLoading(false); + startTransition(() => { + setOverview(ov); setTrends(tr); setUserRank(ur); setModelRank(mr); setLoading(false); + }); }, [granularity, getEffectiveRange]); useEffect(() => { fetchData(); }, [fetchData]); @@ -82,7 +105,7 @@ export default function DashboardPage() {
{([["total_tokens", t("metric.token")], ["calls", t("metric.calls")]] as const).map(([k, l]) => ( -