diff --git a/frontend/src/features/dashboard/components/dashboard-page.tsx b/frontend/src/features/dashboard/components/dashboard-page.tsx index 13a8cf27..7ead2743 100644 --- a/frontend/src/features/dashboard/components/dashboard-page.tsx +++ b/frontend/src/features/dashboard/components/dashboard-page.tsx @@ -135,8 +135,8 @@ export function DashboardPage() { diff --git a/frontend/src/features/dashboard/utils.test.ts b/frontend/src/features/dashboard/utils.test.ts index 1e70cb9a..795548e4 100644 --- a/frontend/src/features/dashboard/utils.test.ts +++ b/frontend/src/features/dashboard/utils.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "vitest"; +import type { AccountSummary, Depletion } from "@/features/dashboard/schemas"; import { applySecondaryConstraint, + buildDashboardView, buildDepletionView, buildRemainingItems, + type RemainingItem, } from "@/features/dashboard/utils"; -import type { RemainingItem } from "@/features/dashboard/utils"; -import type { AccountSummary, Depletion } from "@/features/dashboard/schemas"; +import { createDashboardOverview, createDefaultRequestLogs } from "@/test/mocks/factories"; import { formatCompactAccountId } from "@/utils/account-identifiers"; function account(overrides: Partial & Pick): AccountSummary { @@ -248,3 +250,70 @@ describe("buildRemainingItems", () => { expect(items[2].isEmail).toBe(true); }); }); + +describe("buildDashboardView", () => { + it("derives the primary donut total from the constrained displayed slices", () => { + const overview = createDashboardOverview({ + accounts: [ + account({ + accountId: "acc-1", + email: "one@example.com", + usage: { + primaryRemainingPercent: 90, + secondaryRemainingPercent: 1, + }, + resetAtPrimary: null, + resetAtSecondary: null, + windowMinutesPrimary: 300, + windowMinutesSecondary: 10080, + }), + account({ + accountId: "acc-2", + email: "two@example.com", + usage: { + primaryRemainingPercent: 60, + secondaryRemainingPercent: 70, + }, + resetAtPrimary: null, + resetAtSecondary: null, + windowMinutesPrimary: 300, + windowMinutesSecondary: 10080, + }), + ], + summary: { + primaryWindow: { + remainingPercent: 75, + capacityCredits: 450, + remainingCredits: 337.5, + resetAt: null, + windowMinutes: 300, + }, + secondaryWindow: { + remainingPercent: 35.5, + capacityCredits: 15120, + remainingCredits: 5370, + resetAt: null, + windowMinutes: 10080, + }, + cost: { + currency: "USD", + totalUsd7d: 1.82, + }, + metrics: { + requests7d: 228, + tokensSecondaryWindow: 45000, + cachedTokensSecondaryWindow: 8200, + errorRate7d: 0.028, + topError: "rate_limit_exceeded", + }, + }, + }); + + const view = buildDashboardView(overview, createDefaultRequestLogs(), false); + + expect(view.primaryUsageItems.map((item) => item.value)).toEqual([75.6, 135]); + expect(view.primaryUsageTotal).toBeCloseTo(210.6); + expect(view.primaryUsageTotal).toBeCloseTo(view.primaryUsageItems.reduce((total, item) => total + item.value, 0)); + expect(view.secondaryUsageTotal).toBe(5370); + }); +}); diff --git a/frontend/src/features/dashboard/utils.ts b/frontend/src/features/dashboard/utils.ts index f65f90c0..26d988eb 100644 --- a/frontend/src/features/dashboard/utils.ts +++ b/frontend/src/features/dashboard/utils.ts @@ -1,15 +1,4 @@ -import { Activity, AlertTriangle, Coins, DollarSign } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; - -import { buildDonutPalette } from "@/utils/colors"; -import { buildDuplicateAccountIdSet, formatCompactAccountId } from "@/utils/account-identifiers"; -import { - formatCachedTokensMeta, - formatCompactNumber, - formatCurrency, - formatRate, - formatWindowLabel, -} from "@/utils/formatters"; +import { Activity, AlertTriangle, Coins, DollarSign, type LucideIcon } from "lucide-react"; import type { AccountSummary, @@ -19,6 +8,15 @@ import type { TrendPoint, UsageWindow, } from "@/features/dashboard/schemas"; +import { buildDuplicateAccountIdSet, formatCompactAccountId } from "@/utils/account-identifiers"; +import { buildDonutPalette } from "@/utils/colors"; +import { + formatCachedTokensMeta, + formatCompactNumber, + formatCurrency, + formatRate, + formatWindowLabel, +} from "@/utils/formatters"; export type RemainingItem = { accountId: string; @@ -50,6 +48,8 @@ export type DashboardView = { stats: DashboardStat[]; primaryUsageItems: RemainingItem[]; secondaryUsageItems: RemainingItem[]; + primaryUsageTotal: number; + secondaryUsageTotal: number; requestLogs: RequestLog[]; safeLinePrimary: SafeLineView | null; safeLineSecondary: SafeLineView | null; @@ -165,6 +165,10 @@ function trendPointsToValues(points: TrendPoint[]): { value: number }[] { return points.map((p) => ({ value: p.v })); } +function sumRemainingItems(items: RemainingItem[]): number { + return items.reduce((total, item) => total + item.value, 0); +} + export function buildDashboardView( overview: DashboardOverview, requestLogs: RequestLog[], @@ -216,13 +220,16 @@ export function buildDashboardView( const rawPrimaryItems = buildRemainingItems(overview.accounts, primaryWindow, "primary", isDark); const secondaryUsageItems = buildRemainingItems(overview.accounts, secondaryWindow, "secondary", isDark); + const primaryUsageItems = secondaryWindow + ? applySecondaryConstraint(rawPrimaryItems, secondaryUsageItems) + : rawPrimaryItems; return { stats, - primaryUsageItems: secondaryWindow - ? applySecondaryConstraint(rawPrimaryItems, secondaryUsageItems) - : rawPrimaryItems, + primaryUsageItems, secondaryUsageItems, + primaryUsageTotal: sumRemainingItems(primaryUsageItems), + secondaryUsageTotal: overview.summary.secondaryWindow?.remainingCredits ?? 0, requestLogs, safeLinePrimary: buildDepletionView(overview.depletionPrimary), safeLineSecondary: buildDepletionView(overview.depletionSecondary),