fix(dashboard): show account email/name in Utilization Account Split cards (#10939)

Validado no worktree combinado: typecheck:core, changelog-integrity, complexity, cognitive-complexity, file-size, lint todos verdes. Fix de UX real e bem documentado (cards de Account Split mostravam UUID cru em vez de email/nome da conta). CI vermelho é o base-red já rastreado em #9985. Obrigado!
This commit is contained in:
ZaimMarzuki
2026-08-21 14:24:10 +07:00
committed by GitHub
parent 10deb5c307
commit c0fd109e30
3 changed files with 54 additions and 4 deletions

View File

@@ -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 (
<Card.Section key={point.provider} className="flex h-full flex-col gap-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-black/5 bg-surface text-text-main dark:border-white/5">
<ProviderIcon providerId={point.provider} size={22} />
<ProviderIcon providerId={providerPart} size={22} />
</div>
<div>
<p className="text-sm font-semibold text-text-main">
{resolveProviderName(point.provider, nodeMap)}
{cardTitle}
</p>
<p className="text-xs text-text-muted">
{t("providerUtilizationLatestSnapshot")}
{cardSubtitle}
</p>
</div>
</div>

View File

@@ -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<string, ConnectionMetaEntry> | 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);

View File

@@ -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<string, ConnectionMetaEntry>;
}
export interface ComboHealthMetrics {