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),