From 5cc234ddd28d848d61cb2023135f0ab8c749a87c Mon Sep 17 00:00:00 2001 From: NieiR Date: Mon, 16 Feb 2026 01:09:33 +0800 Subject: [PATCH 1/5] fix(logs): compact stats panel and collapse filters to reduce above-fold space Rebased on upstream v0.5.8 (includes hasStatsFilters perf guard). Retains our UX optimizations for issue #766: - Stats panel: compact single-line inline layout (~40px vs ~150px) - Filter criteria: collapsed by default with active filter count badge - Active sessions: compactEmpty single-line when no sessions --- .../logs/_components/usage-logs-sections.tsx | 1 + .../_components/usage-logs-stats-panel.tsx | 176 ++++++------------ .../usage-logs-view-virtualized.tsx | 82 +++++--- .../customs/active-sessions-list.tsx | 15 ++ 4 files changed, 123 insertions(+), 151 deletions(-) diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx index 0c1ef1d5c..6bf7c2afa 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx @@ -19,6 +19,7 @@ export async function UsageLogsActiveSessionsSection() { currencyCode={systemSettings.currencyDisplay} maxHeight="200px" showTokensCost={false} + compactEmpty /> ); } diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx index b5d7d63f9..c82de01fe 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; import { getUsageLogsStats } from "@/actions/usage-logs"; import { Skeleton } from "@/components/ui/skeleton"; -import { cn, formatTokenAmount } from "@/lib/utils"; +import { formatTokenAmount } from "@/lib/utils"; import type { CurrencyCode } from "@/lib/utils/currency"; import { formatCurrency } from "@/lib/utils/currency"; import type { UsageLogSummary } from "@/repository/usage-logs"; @@ -27,21 +27,14 @@ interface UsageLogsStatsPanelProps { currencyCode?: CurrencyCode; } -/** - * Stats panel component with glass morphism UI - * Always expanded (not collapsible), loads data asynchronously - * Re-fetches when filters change - */ export function UsageLogsStatsPanel({ filters, currencyCode = "USD" }: UsageLogsStatsPanelProps) { const t = useTranslations("dashboard"); const [stats, setStats] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // Create stable filter key for dependency comparison const filtersKey = JSON.stringify(filters); - // Load stats data const loadStats = useCallback(async () => { setIsLoading(true); setError(null); @@ -61,79 +54,30 @@ export function UsageLogsStatsPanel({ filters, currencyCode = "USD" }: UsageLogs } }, [filters, t]); - // Load data on mount and when filters change // biome-ignore lint/correctness/useExhaustiveDependencies: filtersKey is used to detect filter changes useEffect(() => { loadStats(); }, [filtersKey, loadStats]); return ( -
- {/* Glassmorphism gradient overlay */} -
- -
- {/* Header */} -
- - - -
-

- {t("logs.stats.title")} -

-

- {t("logs.stats.description")} -

-
-
- - {/* Content */} -
- {isLoading ? ( - - ) : error ? ( -
{error}
- ) : stats ? ( - - ) : null} -
-
-
- ); -} - -/** - * Stats data skeletons - */ -function StatsSkeletons() { - return ( -
- {[1, 2, 3, 4].map((i) => ( -
+
+ + {isLoading ? ( +
- + + +
- ))} + ) : error ? ( + {error} + ) : stats ? ( + + ) : null}
); } -/** - * Stats data content - */ function StatsContent({ stats, currencyCode, @@ -144,58 +88,48 @@ function StatsContent({ const t = useTranslations("dashboard"); return ( -
- {/* Total Requests */} -
-
{t("logs.stats.totalRequests")}
-
- {stats.totalRequests.toLocaleString()} -
-
- - {/* Total Amount */} -
-
{t("logs.stats.totalAmount")}
-
- {formatCurrency(stats.totalCost, currencyCode)} -
-
- - {/* Total Tokens */} -
-
{t("logs.stats.totalTokens")}
-
- {formatTokenAmount(stats.totalTokens)} -
-
-
- {t("logs.stats.input")}: - {formatTokenAmount(stats.totalInputTokens)} -
-
- {t("logs.stats.output")}: - {formatTokenAmount(stats.totalOutputTokens)} -
-
-
- - {/* Cache Tokens */} -
-
{t("logs.stats.cacheTokens")}
-
- {formatTokenAmount(stats.totalCacheCreationTokens + stats.totalCacheReadTokens)} -
-
-
- {t("logs.stats.write")}: - {formatTokenAmount(stats.totalCacheCreationTokens)} -
-
- {t("logs.stats.read")}: - {formatTokenAmount(stats.totalCacheReadTokens)} -
-
-
+
+ + + + + + + + + + + + +
); } + +function StatItem({ label, value, detail }: { label: string; value: string; detail?: string }) { + return ( + + {label} + {value} + {detail ? ({detail}) : null} + + ); +} + +function Separator() { + return |; +} diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx index 1d64e0e61..8253457f4 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx @@ -1,7 +1,16 @@ "use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Expand, Filter, ListOrdered, Minimize2, Pause, Play, RefreshCw } from "lucide-react"; +import { + ChevronDown, + Expand, + Filter, + ListOrdered, + Minimize2, + Pause, + Play, + RefreshCw, +} from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -10,8 +19,10 @@ import { getKeys } from "@/actions/keys"; import type { OverviewData } from "@/actions/overview"; import { getOverviewData } from "@/actions/overview"; import { getProviders } from "@/actions/providers"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Switch } from "@/components/ui/switch"; import { useFullscreen } from "@/hooks/use-fullscreen"; import { getHiddenColumns, type LogsTableColumn } from "@/lib/column-visibility"; @@ -81,6 +92,7 @@ function UsageLogsViewContent({ const [hideProviderColumn, setHideProviderColumn] = useState(false); const wasInFullscreenRef = useRef(false); const [hiddenColumns, setHiddenColumns] = useState([]); + const [isFiltersOpen, setIsFiltersOpen] = useState(false); // Load initial hidden columns from localStorage useEffect(() => { @@ -266,6 +278,10 @@ function UsageLogsViewContent({ const hasStatsFilters = Object.values(statsFilters).some((v) => v !== undefined && v !== false); + const activeFilterCount = Object.values(statsFilters).filter( + (v) => v !== undefined && v !== false + ).length; + return ( <>
@@ -274,35 +290,41 @@ function UsageLogsViewContent({ )} - {/* Filter Criteria */} - - -
-
- -
-
- {t("title.filterCriteria")} - - {t("title.filterCriteriaDescription")} - -
-
-
- - router.push("/dashboard/logs")} - isProvidersLoading={isProvidersLoading} - isKeysLoading={isKeysLoading} - serverTimeZone={serverTimeZone} - /> - -
+ {/* Filter Criteria - Collapsible */} + + + + + + + + router.push("/dashboard/logs")} + isProvidersLoading={isProvidersLoading} + isKeysLoading={isKeysLoading} + serverTimeZone={serverTimeZone} + /> + + + + {/* Usage Records Table */} diff --git a/src/components/customs/active-sessions-list.tsx b/src/components/customs/active-sessions-list.tsx index be6a22a0b..5c8466b4d 100644 --- a/src/components/customs/active-sessions-list.tsx +++ b/src/components/customs/active-sessions-list.tsx @@ -30,6 +30,8 @@ interface ActiveSessionsListProps { maxHeight?: string; /** 是否显示 Token/成本(默认显示) */ showTokensCost?: boolean; + /** 空状态时压缩为单行 */ + compactEmpty?: boolean; /** 自定义类名 */ className?: string; } @@ -45,6 +47,7 @@ export function ActiveSessionsList({ maxItems, showHeader = true, maxHeight = "200px", + compactEmpty = false, showTokensCost = true, className = "", }: ActiveSessionsListProps) { @@ -63,6 +66,18 @@ export function ActiveSessionsList({ // 实际的活跃 session 总数(用于计数显示) const totalCount = data.length; + if (compactEmpty && !isLoading && totalCount === 0) { + return ( +
+ + {tc("activeSessions.title")} + {tc("activeSessions.empty")} +
+ ); + } + return (
{showHeader && ( From c04715707df0eea035bcb64644bcfc3fa9f6be99 Mon Sep 17 00:00:00 2001 From: NieiR Date: Tue, 17 Feb 2026 18:12:40 +0800 Subject: [PATCH 2/5] fix(leaderboard): SQL AT TIME ZONE operator precedence causes query failure (#800) Add parentheses around date arithmetic in buildDateCondition to prevent AT TIME ZONE from binding to INTERVAL instead of the full expression. --- src/repository/leaderboard.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/repository/leaderboard.ts b/src/repository/leaderboard.ts index 66e2981ed..ed63b959f 100644 --- a/src/repository/leaderboard.ts +++ b/src/repository/leaderboard.ts @@ -174,7 +174,7 @@ function buildDateCondition( return sql`1=1`; case "daily": { const startLocal = sql`DATE_TRUNC('day', ${nowLocal})`; - const endExclusiveLocal = sql`${startLocal} + INTERVAL '1 day'`; + const endExclusiveLocal = sql`(${startLocal} + INTERVAL '1 day')`; const start = sql`(${startLocal} AT TIME ZONE ${timezone})`; const endExclusive = sql`(${endExclusiveLocal} AT TIME ZONE ${timezone})`; return sql`${messageRequest.createdAt} >= ${start} AND ${messageRequest.createdAt} < ${endExclusive}`; @@ -183,14 +183,14 @@ function buildDateCondition( return sql`${messageRequest.createdAt} >= (CURRENT_TIMESTAMP - INTERVAL '24 hours')`; case "weekly": { const startLocal = sql`DATE_TRUNC('week', ${nowLocal})`; - const endExclusiveLocal = sql`${startLocal} + INTERVAL '1 week'`; + const endExclusiveLocal = sql`(${startLocal} + INTERVAL '1 week')`; const start = sql`(${startLocal} AT TIME ZONE ${timezone})`; const endExclusive = sql`(${endExclusiveLocal} AT TIME ZONE ${timezone})`; return sql`${messageRequest.createdAt} >= ${start} AND ${messageRequest.createdAt} < ${endExclusive}`; } case "monthly": { const startLocal = sql`DATE_TRUNC('month', ${nowLocal})`; - const endExclusiveLocal = sql`${startLocal} + INTERVAL '1 month'`; + const endExclusiveLocal = sql`(${startLocal} + INTERVAL '1 month')`; const start = sql`(${startLocal} AT TIME ZONE ${timezone})`; const endExclusive = sql`(${endExclusiveLocal} AT TIME ZONE ${timezone})`; return sql`${messageRequest.createdAt} >= ${start} AND ${messageRequest.createdAt} < ${endExclusive}`; From 45ffb062037f8ea122dbbeb5d01386f7011b5a99 Mon Sep 17 00:00:00 2001 From: NieiR Date: Thu, 19 Feb 2026 08:29:52 +0800 Subject: [PATCH 3/5] fix(logs): stabilize statsFilters reference, align filter count, and auto-expand on URL filters - Wrap statsFilters in useMemo to prevent unnecessary API re-requests when parent re-renders without filter changes - Replace Object.values filter count with explicit per-field counting, grouping statusCode/excludeStatusCode200 as one filter (consistent with usage-logs-filters.tsx) - Auto-expand collapsible filter panel when URL-based filters are detected on initial page load --- .../usage-logs-view-virtualized.tsx | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx index 8253457f4..a077c5c0d 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx @@ -262,25 +262,47 @@ function UsageLogsViewContent({ }; }, []); - const statsFilters = { - userId: filters.userId, - keyId: filters.keyId, - providerId: filters.providerId, - sessionId: filters.sessionId, - startTime: filters.startTime, - endTime: filters.endTime, - statusCode: filters.statusCode, - excludeStatusCode200: filters.excludeStatusCode200, - model: filters.model, - endpoint: filters.endpoint, - minRetryCount: filters.minRetryCount, - }; + const statsFilters = useMemo( + () => ({ + userId: filters.userId, + keyId: filters.keyId, + providerId: filters.providerId, + sessionId: filters.sessionId, + startTime: filters.startTime, + endTime: filters.endTime, + statusCode: filters.statusCode, + excludeStatusCode200: filters.excludeStatusCode200, + model: filters.model, + endpoint: filters.endpoint, + minRetryCount: filters.minRetryCount, + }), + [filters] + ); const hasStatsFilters = Object.values(statsFilters).some((v) => v !== undefined && v !== false); - const activeFilterCount = Object.values(statsFilters).filter( - (v) => v !== undefined && v !== false - ).length; + const activeFilterCount = useMemo(() => { + let count = 0; + if (filters.userId !== undefined) count++; + if (filters.keyId !== undefined) count++; + if (filters.providerId !== undefined) count++; + if (filters.sessionId) count++; + if (filters.startTime && filters.endTime) count++; + if (filters.statusCode !== undefined || filters.excludeStatusCode200) count++; + if (filters.model) count++; + if (filters.endpoint) count++; + if (filters.minRetryCount !== undefined && filters.minRetryCount > 0) count++; + return count; + }, [filters]); + + // Auto-expand filter panel when URL-based filters are present on initial load + const filterAutoExpandRef = useRef(true); + useEffect(() => { + if (filterAutoExpandRef.current && activeFilterCount > 0) { + setIsFiltersOpen(true); + } + filterAutoExpandRef.current = false; + }, [activeFilterCount]); return ( <> From 2be6f61760e31ce1725cefbb8938360c9f883e6f Mon Sep 17 00:00:00 2001 From: NieiR Date: Thu, 19 Feb 2026 08:43:07 +0800 Subject: [PATCH 4/5] fix(i18n): use fullwidth parentheses in zh-TW cacheTtlSwapped Half-width parentheses were introduced in #798, breaking the zh-tw-dashboard-parentheses test. --- messages/zh-TW/dashboard.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index abdafbccf..4acf6a3ed 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -280,7 +280,7 @@ "cacheWrite1h": "快取寫入(1h)", "cacheRead": "快取讀取", "cacheTtl": "快取 TTL", - "cacheTtlSwapped": "計費 TTL (已互換)", + "cacheTtlSwapped": "計費 TTL(已互換)", "multiplier": "供應商倍率", "totalCost": "總費用", "context1m": "1M 上下文", @@ -366,7 +366,7 @@ "cacheWrite1h": "快取寫入(1h)", "cacheRead": "快取讀取", "cacheTtl": "快取 TTL", - "cacheTtlSwapped": "計費 TTL (已互換)", + "cacheTtlSwapped": "計費 TTL(已互換)", "multiplier": "供應商倍率", "totalCost": "總費用", "context1m": "1M 上下文長度", From 5e2c8d9ec02207a4f21ccbe6920d8f4460ba7b0e Mon Sep 17 00:00:00 2001 From: NieiR Date: Thu, 19 Feb 2026 08:47:39 +0800 Subject: [PATCH 5/5] style: fix import ordering from upstream merge --- .../settings/providers/_components/add-provider-dialog.tsx | 2 +- .../settings/providers/_components/provider-rich-list-item.tsx | 2 +- .../settings/providers/_components/vendor-keys-compact-list.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx index e8d944292..917d30a05 100644 --- a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx +++ b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx @@ -1,11 +1,11 @@ "use client"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { ServerCog } from "lucide-react"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { FormErrorBoundary } from "@/components/form-error-boundary"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { ProviderForm } from "./forms/provider-form"; interface AddProviderDialogProps { diff --git a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx index 6ff5e67e6..aeea38060 100644 --- a/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx +++ b/src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx @@ -1,4 +1,5 @@ "use client"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, @@ -15,7 +16,6 @@ import { import { useTranslations } from "next-intl"; import { useEffect, useState, useTransition } from "react"; import { toast } from "sonner"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { editProvider, getUnmaskedProviderKey, diff --git a/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx b/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx index acc70cfa4..da1b47b02 100644 --- a/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx +++ b/src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx @@ -1,11 +1,11 @@ "use client"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CheckCircle, Copy, Edit2, Loader2, Plus, Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { getProviderEndpoints } from "@/actions/provider-endpoints"; import { editProvider, getUnmaskedProviderKey, removeProvider } from "@/actions/providers"; import { FormErrorBoundary } from "@/components/form-error-boundary";