diff --git a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx index b8d62d96ce..84e99583c1 100644 --- a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx +++ b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx @@ -3,6 +3,7 @@ import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useProviderNodeMap, resolveProviderName } from "@/lib/display/useProviderNodeMap"; +import { getAccountDisplayName } from "@/lib/display/names"; import dynamic from "next/dynamic"; const ProviderCharts = dynamic(() => import("./components/ProviderCharts"), { ssr: false }); @@ -311,19 +312,39 @@ export default function ProviderUtilizationTab() { {latestPoints.map((point) => { const isLow = point.remainingPct <= 20; + // For Account Split, parse "provider:connectionId" and resolve display name + const colonIdx = point.provider.indexOf(":"); + const isConnectionKey = aggregateBy === "connection" && colonIdx !== -1; + const providerPart = isConnectionKey + ? point.provider.slice(0, colonIdx) + : point.provider; + const connectionId = isConnectionKey ? point.provider.slice(colonIdx + 1) : null; + const connMeta = connectionId ? data?.connectionMeta?.[connectionId] : null; + const cardTitle = isConnectionKey + ? getAccountDisplayName({ + id: connectionId ?? undefined, + email: connMeta?.email, + name: connMeta?.name, + displayName: connMeta?.displayName, + }) + : resolveProviderName(point.provider, nodeMap); + const cardSubtitle = isConnectionKey + ? `${providerPart} · account ${(connectionId ?? "").slice(0, 8)}…` + : t("providerUtilizationLatestSnapshot"); + return (
- +

- {resolveProviderName(point.provider, nodeMap)} + {cardTitle}

- {t("providerUtilizationLatestSnapshot")} + {cardSubtitle}

diff --git a/src/app/api/usage/utilization/route.ts b/src/app/api/usage/utilization/route.ts index 4d602fefe2..40b8454142 100644 --- a/src/app/api/usage/utilization/route.ts +++ b/src/app/api/usage/utilization/route.ts @@ -1,6 +1,11 @@ import { NextResponse } from "next/server"; import { getAggregatedSnapshots } from "@/lib/db/quotaSnapshots"; -import type { ProviderUtilizationResponse, UtilizationTimeRange } from "@/shared/types/utilization"; +import { getConnection } from "@/lib/db/connections"; +import type { + ProviderUtilizationResponse, + UtilizationTimeRange, + ConnectionMetaEntry, +} from "@/shared/types/utilization"; import { BUCKET_SIZES } from "@/shared/types/utilization"; const VALID_RANGES: UtilizationTimeRange[] = ["1h", "24h", "7d", "30d"]; @@ -55,11 +60,28 @@ export async function GET(request: Request) { const providers = Array.from(new Set(data.map((d) => d.provider))); + let connectionMeta: Record | undefined; + if (aggregateBy === "connection") { + const uniqueConnectionIds = new Set( + data.map((d) => d.provider.split(":").slice(1).join(":")).filter(Boolean) + ); + connectionMeta = {}; + for (const cid of uniqueConnectionIds) { + const conn = getConnection(cid); + connectionMeta[cid] = { + email: conn?.email ?? null, + name: conn?.name ?? null, + displayName: conn?.displayName ?? null, + }; + } + } + const response: ProviderUtilizationResponse = { timeRange: range, bucketSizeMinutes: bucketMinutes, providers, data, + connectionMeta, }; return NextResponse.json(response); diff --git a/src/shared/types/utilization.ts b/src/shared/types/utilization.ts index 4b637e7385..36f0d7dabc 100644 --- a/src/shared/types/utilization.ts +++ b/src/shared/types/utilization.ts @@ -19,11 +19,18 @@ export interface ProviderUtilizationPoint { windowKey: string; } +export interface ConnectionMetaEntry { + email: string | null; + name: string | null; + displayName: string | null; +} + export interface ProviderUtilizationResponse { timeRange: "1h" | "24h" | "7d" | "30d"; bucketSizeMinutes: number; providers: string[]; data: ProviderUtilizationPoint[]; + connectionMeta?: Record; } export interface ComboHealthMetrics {