Przeglądaj źródła

fix: prevent TypeError when calling toFixed on string values from PostgreSQL

PostgreSQL numeric/decimal columns serialize as strings in JSON responses,
causing .toFixed() to throw TypeError when called on string values.

Changes:
- Wrap all .toFixed() calls with Number() for type coercion
- Add division by zero guards for percentage calculations
- Add Number.isFinite() checks for critical display values
- Standardize percentage decimal places to 1 digit for consistency

Affected components:
- my-usage: quota-cards, usage-logs-table
- dashboard: logs, sessions, availability, leaderboard, rate-limits
- quotas: keys, users, providers dialogs and list items
- internal: big-screen dashboard
- shared: session-card, session-list-item, overview-panel, user-quota-header
ding113 2 miesięcy temu
rodzic
commit
be63aa5ea3

+ 2 - 1
src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx

@@ -126,7 +126,8 @@ export function RateLimitTopUsers({ data }: RateLimitTopUsersProps) {
               </TableHeader>
               <TableBody>
                 {tableData.map((row, index) => {
-                  const percentage = ((row.eventCount / totalEvents) * 100).toFixed(1);
+                  const percentage =
+                    totalEvents > 0 ? ((row.eventCount / totalEvents) * 100).toFixed(1) : "0.0";
                   return (
                     <TableRow key={row.userId}>
                       <TableCell className="font-medium">#{index + 1}</TableCell>

+ 2 - 1
src/app/[locale]/dashboard/_components/rate-limit-type-breakdown.tsx

@@ -77,7 +77,8 @@ export function RateLimitTypeBreakdown({ data }: RateLimitTypeBreakdownProps) {
                   if (!active || !payload || !payload.length) return <div className="hidden" />;
 
                   const data = payload[0].payload;
-                  const percentage = ((data.count / totalEvents) * 100).toFixed(1);
+                  const percentage =
+                    totalEvents > 0 ? ((data.count / totalEvents) * 100).toFixed(1) : "0.0";
 
                   return (
                     <div className="rounded-lg border bg-background p-3 shadow-sm">

+ 3 - 3
src/app/[locale]/dashboard/availability/_components/availability-view.tsx

@@ -95,7 +95,7 @@ function formatBucketTime(isoString: string, bucketSizeMinutes: number): string
 function _formatBucketSizeDisplay(minutes: number): string {
   if (minutes >= 60) {
     const hours = minutes / 60;
-    return hours === 1 ? "1 hour" : `${hours.toFixed(1)} hours`;
+    return hours === 1 ? "1 hour" : `${Number(hours).toFixed(1)} hours`;
   }
   if (minutes >= 1) {
     return `${Math.round(minutes)} min`;
@@ -232,10 +232,10 @@ export function AvailabilityView() {
 
   const formatLatency = (ms: number) => {
     if (ms < 1000) return `${Math.round(ms)}ms`;
-    return `${(ms / 1000).toFixed(2)}s`;
+    return `${(Number(ms) / 1000).toFixed(2)}s`;
   };
 
-  const formatPercentage = (value: number) => `${(value * 100).toFixed(1)}%`;
+  const formatPercentage = (value: number) => `${(Number(value) * 100).toFixed(1)}%`;
 
   // Summary counts
   const getSummaryCounts = () => {

+ 2 - 2
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx

@@ -190,7 +190,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
     {
       header: t("columns.successRate"),
       className: "text-right",
-      cell: (row) => `${(((row as ProviderEntry).successRate || 0) * 100).toFixed(1)}%`,
+      cell: (row) => `${(Number((row as ProviderEntry).successRate || 0) * 100).toFixed(1)}%`,
     },
     {
       header: t("columns.avgResponseTime"),
@@ -230,7 +230,7 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
     {
       header: t("columns.successRate"),
       className: "text-right",
-      cell: (row) => `${(((row as ModelEntry).successRate || 0) * 100).toFixed(1)}%`,
+      cell: (row) => `${(Number((row as ModelEntry).successRate || 0) * 100).toFixed(1)}%`,
     },
   ];
 

+ 1 - 1
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -36,7 +36,7 @@ function formatDuration(durationMs: number | null): string {
 
   // 1000ms 以上转换为秒
   if (durationMs >= 1000) {
-    return `${(durationMs / 1000).toFixed(2)}s`;
+    return `${(Number(durationMs) / 1000).toFixed(2)}s`;
   }
 
   // 1000ms 以下显示毫秒

+ 8 - 8
src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx

@@ -178,8 +178,8 @@ export function EditKeyQuotaDialog({
                   <p className="text-xs text-muted-foreground">
                     {t("cost5h.current", {
                       currency: currencySymbol,
-                      current: currentQuota.cost5h.current.toFixed(4),
-                      limit: currentQuota.cost5h.limit.toFixed(2),
+                      current: Number(currentQuota.cost5h.current).toFixed(4),
+                      limit: Number(currentQuota.cost5h.limit).toFixed(2),
                     })}
                   </p>
                 )}
@@ -204,8 +204,8 @@ export function EditKeyQuotaDialog({
                   <p className="text-xs text-muted-foreground">
                     {t("costDaily.current", {
                       currency: currencySymbol,
-                      current: currentQuota.costDaily.current.toFixed(4),
-                      limit: currentQuota.costDaily.limit.toFixed(2),
+                      current: Number(currentQuota.costDaily.current).toFixed(4),
+                      limit: Number(currentQuota.costDaily.limit).toFixed(2),
                     })}
                   </p>
                 )}
@@ -272,8 +272,8 @@ export function EditKeyQuotaDialog({
                   <p className="text-xs text-muted-foreground">
                     {t("costWeekly.current", {
                       currency: currencySymbol,
-                      current: currentQuota.costWeekly.current.toFixed(4),
-                      limit: currentQuota.costWeekly.limit.toFixed(2),
+                      current: Number(currentQuota.costWeekly.current).toFixed(4),
+                      limit: Number(currentQuota.costWeekly.limit).toFixed(2),
                     })}
                   </p>
                 )}
@@ -298,8 +298,8 @@ export function EditKeyQuotaDialog({
                   <p className="text-xs text-muted-foreground">
                     {t("costMonthly.current", {
                       currency: currencySymbol,
-                      current: currentQuota.costMonthly.current.toFixed(4),
-                      limit: currentQuota.costMonthly.limit.toFixed(2),
+                      current: Number(currentQuota.costMonthly.current).toFixed(4),
+                      limit: Number(currentQuota.costMonthly.limit).toFixed(2),
                     })}
                   </p>
                 )}

+ 2 - 2
src/app/[locale]/dashboard/quotas/keys/_components/edit-user-quota-dialog.tsx

@@ -142,8 +142,8 @@ export function EditUserQuotaDialog({
                   <p className="text-xs text-muted-foreground">
                     {t("dailyQuota.current", {
                       currency: currencySymbol,
-                      current: currentQuota.dailyCost.current.toFixed(4),
-                      limit: currentQuota.dailyCost.limit.toFixed(2),
+                      current: Number(currentQuota.dailyCost.current).toFixed(4),
+                      limit: Number(currentQuota.dailyCost.limit).toFixed(2),
                     })}
                   </p>
                 )}

+ 2 - 2
src/app/[locale]/dashboard/quotas/providers/_components/provider-quota-list-item.tsx

@@ -86,7 +86,7 @@ export function ProviderQuotaListItem({
                 {t("list.limit")}: {formatCurrency(limit, currencyCode)}
               </div>
               <div className="text-xs font-semibold">
-                {percentage.toFixed(1)}% {t("list.used")}
+                {Number(percentage).toFixed(1)}% {t("list.used")}
               </div>
             </div>
           </TooltipContent>
@@ -128,7 +128,7 @@ export function ProviderQuotaListItem({
                 {t("list.limit")}: {limit}
               </div>
               <div className="text-xs font-semibold">
-                {percentage.toFixed(1)}% {t("list.used")}
+                {Number(percentage).toFixed(1)}% {t("list.used")}
               </div>
             </div>
           </TooltipContent>

+ 1 - 1
src/app/[locale]/dashboard/quotas/users/_components/user-quota-list-item.tsx

@@ -228,7 +228,7 @@ export function UserQuotaListItem({ user, currencyCode = "USD" }: UserQuotaListI
                   </span>
                 </div>
                 <QuotaProgress current={user.totalUsage} limit={totalLimit} className="h-2" />
-                <p className="text-xs text-muted-foreground">{totalProgress.toFixed(1)}%</p>
+                <p className="text-xs text-muted-foreground">{Number(totalProgress).toFixed(1)}%</p>
               </div>
             ) : (
               <p className="font-medium">{t("noLimitSet")}</p>

+ 1 - 1
src/app/[locale]/dashboard/rate-limits/_components/rate-limit-dashboard.tsx

@@ -96,7 +96,7 @@ export function RateLimitDashboard(_props: RateLimitDashboardProps) {
             </div>
             <div className="rounded-lg border bg-card p-4">
               <div className="text-sm text-muted-foreground">{t("avgUsage")}</div>
-              <div className="text-2xl font-bold">{stats.avg_current_usage.toFixed(1)}%</div>
+              <div className="text-2xl font-bold">{(stats.avg_current_usage ?? 0).toFixed(1)}%</div>
             </div>
             <div className="rounded-lg border bg-card p-4">
               <div className="text-sm text-muted-foreground">{t("affectedUsers")}</div>

+ 1 - 1
src/app/[locale]/dashboard/sessions/[sessionId]/messages/page.tsx

@@ -403,7 +403,7 @@ export default function SessionMessagesPage() {
                             <code className="text-sm font-mono font-semibold">
                               {sessionStats.totalDurationMs < 1000
                                 ? `${sessionStats.totalDurationMs}ms`
-                                : `${(sessionStats.totalDurationMs / 1000).toFixed(2)}s`}
+                                : `${(Number(sessionStats.totalDurationMs) / 1000).toFixed(2)}s`}
                             </code>
                           </div>
                         </>

+ 1 - 1
src/app/[locale]/dashboard/sessions/_components/active-sessions-table.tsx

@@ -46,7 +46,7 @@ function formatDuration(durationMs: number | undefined): string {
   if (durationMs < 1000) {
     return `${durationMs}ms`;
   } else if (durationMs < 60000) {
-    return `${(durationMs / 1000).toFixed(1)}s`;
+    return `${(Number(durationMs) / 1000).toFixed(1)}s`;
   } else {
     const minutes = Math.floor(durationMs / 60000);
     const seconds = Math.floor((durationMs % 60000) / 1000);

+ 6 - 4
src/app/[locale]/internal/dashboard/big-screen/page.tsx

@@ -191,7 +191,7 @@ const CountUp = ({
   return (
     <span className={`font-mono ${className}`}>
       {prefix}
-      {displayValue.toFixed(decimals)}
+      {Number(displayValue).toFixed(decimals)}
       {suffix}
     </span>
   );
@@ -467,7 +467,7 @@ const UserRankings = ({
               <div className="flex justify-between items-center">
                 <span className={`text-xs font-bold truncate ${theme.text}`}>{user.userName}</span>
                 <span className="text-[10px] text-gray-500 font-mono">
-                  ${user.totalCost.toFixed(2)}
+                  ${Number(user.totalCost).toFixed(2)}
                 </span>
               </div>
               <div className="flex justify-between items-center mt-1">
@@ -522,7 +522,9 @@ const ProviderRanking = ({
               <span className={`text-xs font-semibold ${theme.text}`}>{p.providerName}</span>
             </div>
             <div className="text-right">
-              <div className={`text-xs font-mono ${theme.accent}`}>${p.totalCost.toFixed(2)}</div>
+              <div className={`text-xs font-mono ${theme.accent}`}>
+                ${Number(p.totalCost).toFixed(2)}
+              </div>
               <div className="text-[9px] text-gray-500">
                 {p.totalTokens.toLocaleString()} Tokens
               </div>
@@ -851,7 +853,7 @@ export default function BigScreenPage() {
           />
           <MetricCard
             title={t("metrics.errorRate")}
-            value={`${metrics.todayErrorRate.toFixed(2)}%`}
+            value={`${Number(metrics.todayErrorRate).toFixed(1)}%`}
             subValue={metrics.todayErrorRate > 2 ? "High" : "Normal"}
             type={metrics.todayErrorRate > 2 ? "negative" : "neutral"}
             icon={AlertTriangle}

+ 7 - 2
src/app/[locale]/my-usage/_components/quota-cards.tsx

@@ -210,8 +210,13 @@ function QuotaColumn({
   muted?: boolean;
 }) {
   const t = useTranslations("myUsage.quota");
-  const formatValue = (value: number) =>
-    currency ? `${currency} ${value.toFixed(2)}` : value.toString();
+  const formatValue = (value: number) => {
+    const num = Number(value);
+    if (!Number.isFinite(num)) {
+      return currency ? `${currency} 0.00` : "0";
+    }
+    return currency ? `${currency} ${num.toFixed(2)}` : String(num);
+  };
 
   const progressClass = `h-2 ${
     tone === "danger"

+ 1 - 1
src/app/[locale]/my-usage/_components/usage-logs-table.tsx

@@ -77,7 +77,7 @@ export function UsageLogsTable({
                     {log.inputTokens}/{log.outputTokens}
                   </TableCell>
                   <TableCell className="text-right text-sm font-mono">
-                    {currencyCode} {(log.cost ?? 0).toFixed(4)}
+                    {currencyCode} {Number(log.cost ?? 0).toFixed(4)}
                   </TableCell>
                   <TableCell>
                     <Badge

+ 1 - 1
src/components/customs/overview-panel.tsx

@@ -45,7 +45,7 @@ export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: Overvie
   // 格式化响应时间
   const formatResponseTime = (ms: number) => {
     if (ms < 1000) return `${ms}ms`;
-    return `${(ms / 1000).toFixed(1)}s`;
+    return `${(Number(ms) / 1000).toFixed(1)}s`;
   };
 
   const metrics = data || {

+ 1 - 1
src/components/customs/session-card.tsx

@@ -24,7 +24,7 @@ function formatDuration(durationMs: number | undefined): string {
   if (durationMs < 1000) {
     return `${durationMs}ms`;
   } else if (durationMs < 60000) {
-    return `${(durationMs / 1000).toFixed(1)}s`;
+    return `${(Number(durationMs) / 1000).toFixed(1)}s`;
   } else {
     const minutes = Math.floor(durationMs / 60000);
     const seconds = Math.floor((durationMs % 60000) / 1000);

+ 1 - 1
src/components/customs/session-list-item.tsx

@@ -16,7 +16,7 @@ function formatDuration(durationMs: number | undefined): string {
   if (durationMs < 1000) {
     return `${durationMs}ms`;
   } else if (durationMs < 60000) {
-    return `${(durationMs / 1000).toFixed(1)}s`;
+    return `${(Number(durationMs) / 1000).toFixed(1)}s`;
   } else {
     const minutes = Math.floor(durationMs / 60000);
     const seconds = Math.floor((durationMs % 60000) / 1000);

+ 3 - 3
src/components/quota/user-quota-header.tsx

@@ -109,7 +109,7 @@ export function UserQuotaHeader({
                   {rpmCurrent}/{rpmLimit}
                 </span>
                 <span className="text-xs text-muted-foreground w-12 text-right flex-shrink-0">
-                  {rpmRate.toFixed(1)}%
+                  {Number(rpmRate).toFixed(1)}%
                 </span>
               </div>
 
@@ -124,10 +124,10 @@ export function UserQuotaHeader({
                   className="flex-1"
                 />
                 <span className="text-sm font-mono w-24 text-right flex-shrink-0">
-                  ${dailyCostCurrent.toFixed(2)}/${dailyCostLimit}
+                  ${Number(dailyCostCurrent).toFixed(2)}/${dailyCostLimit}
                 </span>
                 <span className="text-xs text-muted-foreground w-12 text-right flex-shrink-0">
-                  {dailyRate.toFixed(1)}%
+                  {Number(dailyRate).toFixed(1)}%
                 </span>
               </div>
             </div>