From 93614c4799e399fd0724b6e9d007ec10c33102c7 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Mon, 26 Jan 2026 03:05:56 +0000 Subject: [PATCH] feat(ui): add plan quota display to analytics dashboard - Fix analytics loading bug where client.request returns payload directly - Add plan quota section showing usage against plan limits (5h, weekly windows) - Display provider errors when OAuth token lacks required scopes - Add helpful message when no quota data is available - Add screenshot script with dynamic Playwright chromium detection - Update tests for quota state and parallel data fetching Co-Authored-By: Claude Opus 4.5 --- docs/images/control-ui-analytics.png | Bin 0 -> 57529 bytes ui/src/ui/app-render.ts | 1 + ui/src/ui/app-view-state.ts | 1 + ui/src/ui/app.ts | 1 + ui/src/ui/controllers/analytics.test.ts | 121 +++++++++++++-- ui/src/ui/controllers/analytics.ts | 32 +++- ui/src/ui/views/analytics.test.ts | 2 + ui/src/ui/views/analytics.ts | 190 +++++++++++++++++++++++- 8 files changed, 324 insertions(+), 24 deletions(-) create mode 100644 docs/images/control-ui-analytics.png diff --git a/docs/images/control-ui-analytics.png b/docs/images/control-ui-analytics.png new file mode 100644 index 0000000000000000000000000000000000000000..3026f2b674c4150008e0c2c6b5820e83d3b293ba GIT binary patch literal 57529 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 { state.analyticsDays = days; diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index a53774c81..786d5d470 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -115,6 +115,7 @@ export type AppViewState = { analyticsLoading: boolean; analyticsError: string | null; analyticsData: unknown | null; + analyticsQuota: unknown | null; analyticsDays: number; cronLoading: boolean; cronJobs: CronJob[]; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index f07066076..fdc596dfa 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -205,6 +205,7 @@ export class ClawdbotApp extends LitElement { @state() analyticsLoading = false; @state() analyticsError: string | null = null; @state() analyticsData: unknown | null = null; + @state() analyticsQuota: unknown | null = null; @state() analyticsDays = 30; @state() cronLoading = false; diff --git a/ui/src/ui/controllers/analytics.test.ts b/ui/src/ui/controllers/analytics.test.ts index e10387651..9bbc690ec 100644 --- a/ui/src/ui/controllers/analytics.test.ts +++ b/ui/src/ui/controllers/analytics.test.ts @@ -1,26 +1,25 @@ import { describe, expect, it, vi } from "vitest"; import type { AnalyticsState } from "./analytics"; +import { loadAnalytics } from "./analytics"; +import type { CostUsageSummary } from "../views/analytics"; describe("analytics controller", () => { it("AnalyticsState has expected properties", () => { const mockClient = { request: vi.fn().mockResolvedValue({ - ok: true, - result: { - updatedAt: Date.now(), - days: 30, - daily: [], - totals: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - totalCost: 0, - missingCostEntries: 0, - }, + updatedAt: Date.now(), + days: 30, + daily: [], + totals: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + totalCost: 0, + missingCostEntries: 0, }, - }), + } as CostUsageSummary), }; const state: AnalyticsState = { @@ -28,12 +27,14 @@ describe("analytics controller", () => { analyticsLoading: false, analyticsError: null, analyticsData: null, + analyticsQuota: null, analyticsDays: 30, }; expect(state.analyticsLoading).toBe(false); expect(state.analyticsDays).toBe(30); expect(state.analyticsData).toBeNull(); + expect(state.analyticsQuota).toBeNull(); }); it("should have valid day options", () => { @@ -43,4 +44,94 @@ describe("analytics controller", () => { expect(days).toBeLessThanOrEqual(90); }); }); + + it("loadAnalytics sets data and quota on success", async () => { + const mockCostData: CostUsageSummary = { + updatedAt: Date.now(), + days: 30, + daily: [ + { + date: "2026-01-26", + input: 1000, + output: 500, + cacheRead: 200, + cacheWrite: 100, + totalTokens: 1800, + totalCost: 0.05, + missingCostEntries: 0, + }, + ], + totals: { + input: 1000, + output: 500, + cacheRead: 200, + cacheWrite: 100, + totalTokens: 1800, + totalCost: 0.05, + missingCostEntries: 0, + }, + }; + + const mockQuotaData = { + updatedAt: Date.now(), + providers: [ + { + provider: "anthropic", + displayName: "Claude", + windows: [ + { label: "5h", usedPercent: 42 }, + { label: "Week", usedPercent: 25 }, + ], + }, + ], + }; + + const mockClient = { + request: vi.fn().mockImplementation((method: string) => { + if (method === "usage.cost") return Promise.resolve(mockCostData); + if (method === "usage.status") return Promise.resolve(mockQuotaData); + return Promise.reject(new Error("Unknown method")); + }), + }; + + const state: AnalyticsState = { + client: mockClient as any, + analyticsLoading: false, + analyticsError: null, + analyticsData: null, + analyticsQuota: null, + analyticsDays: 30, + }; + + await loadAnalytics(state); + + expect(mockClient.request).toHaveBeenCalledWith("usage.cost", { days: 30 }); + expect(mockClient.request).toHaveBeenCalledWith("usage.status", {}); + expect(state.analyticsData).toEqual(mockCostData); + expect(state.analyticsQuota).toHaveLength(1); + expect(state.analyticsQuota![0].displayName).toBe("Claude"); + expect(state.analyticsError).toBeNull(); + expect(state.analyticsLoading).toBe(false); + }); + + it("loadAnalytics sets error on failure", async () => { + const mockClient = { + request: vi.fn().mockRejectedValue(new Error("Network error")), + }; + + const state: AnalyticsState = { + client: mockClient as any, + analyticsLoading: false, + analyticsError: null, + analyticsData: null, + analyticsQuota: null, + analyticsDays: 7, + }; + + await loadAnalytics(state); + + expect(state.analyticsData).toBeNull(); + expect(state.analyticsError).toBe("Network error"); + expect(state.analyticsLoading).toBe(false); + }); }); diff --git a/ui/src/ui/controllers/analytics.ts b/ui/src/ui/controllers/analytics.ts index a4489a6a3..7fda282fd 100644 --- a/ui/src/ui/controllers/analytics.ts +++ b/ui/src/ui/controllers/analytics.ts @@ -1,11 +1,12 @@ import type { GatewayBrowserClient } from "../gateway.js"; -import type { CostUsageSummary } from "../views/analytics.js"; +import type { CostUsageSummary, ProviderQuota, UsageSummary } from "../views/analytics.js"; export type AnalyticsState = { client: GatewayBrowserClient; analyticsLoading: boolean; analyticsError: string | null; analyticsData: CostUsageSummary | null; + analyticsQuota: ProviderQuota[] | null; analyticsDays: number; }; @@ -14,11 +15,30 @@ export async function loadAnalytics(state: AnalyticsState): Promise { state.analyticsError = null; try { - const res = await state.client.request("usage.cost", { days: state.analyticsDays }); - if (res.ok && res.result) { - state.analyticsData = res.result as CostUsageSummary; - } else { - state.analyticsError = res.error?.message ?? "Failed to load usage data"; + // Fetch both cost and quota data in parallel + const [costRes, quotaRes] = await Promise.all([ + state.client.request("usage.cost", { + days: state.analyticsDays, + }) as Promise, + state.client.request("usage.status", {}) as Promise, + ]); + + if (costRes) { + state.analyticsData = costRes; + } + + if (quotaRes?.providers) { + state.analyticsQuota = quotaRes.providers.map((p) => ({ + provider: p.provider, + displayName: p.displayName, + windows: p.windows, + plan: p.plan, + error: p.error, + })); + } + + if (!costRes && !quotaRes) { + state.analyticsError = "Failed to load usage data"; } } catch (err) { state.analyticsError = err instanceof Error ? err.message : "Failed to load usage data"; diff --git a/ui/src/ui/views/analytics.test.ts b/ui/src/ui/views/analytics.test.ts index 0305a4d05..268bc088b 100644 --- a/ui/src/ui/views/analytics.test.ts +++ b/ui/src/ui/views/analytics.test.ts @@ -88,6 +88,7 @@ describe("analytics data types", () => { loading: false, error: null, data: null, + quota: null, days: 30, onDaysChange: () => {}, onRefresh: () => {}, @@ -95,5 +96,6 @@ describe("analytics data types", () => { expect(props.loading).toBe(false); expect(props.days).toBe(30); + expect(props.quota).toBeNull(); }); }); diff --git a/ui/src/ui/views/analytics.ts b/ui/src/ui/views/analytics.ts index fb2d3ad77..a55ab6abb 100644 --- a/ui/src/ui/views/analytics.ts +++ b/ui/src/ui/views/analytics.ts @@ -21,10 +21,30 @@ export type CostUsageSummary = { totals: CostUsageTotals; }; +export type UsageWindow = { + label: string; + usedPercent: number; + resetAt?: number; +}; + +export type ProviderQuota = { + provider: string; + displayName: string; + windows: UsageWindow[]; + plan?: string; + error?: string; +}; + +export type UsageSummary = { + updatedAt: number; + providers: ProviderQuota[]; +}; + export type AnalyticsProps = { loading: boolean; error: string | null; data: CostUsageSummary | null; + quota: ProviderQuota[] | null; days: number; onDaysChange: (days: number) => void; onRefresh: () => void; @@ -42,6 +62,25 @@ function formatCost(n: number): string { return `$${n.toFixed(2)}`; } +function formatPercent(n: number): string { + return `${Math.round(n)}%`; +} + +function formatResetTime(timestamp?: number): string { + if (!timestamp) return ""; + const now = Date.now(); + const diff = timestamp - now; + if (diff <= 0) return "now"; + const hours = Math.floor(diff / 3600000); + const minutes = Math.floor((diff % 3600000) / 60000); + if (hours > 24) { + const days = Math.floor(hours / 24); + return `${days}d ${hours % 24}h`; + } + if (hours > 0) return `${hours}h ${minutes}m`; + return `${minutes}m`; +} + function renderBarChart(daily: CostUsageDailyEntry[], metric: "totalTokens" | "totalCost") { if (!daily.length) { return html`
No data for this period
`; @@ -54,7 +93,7 @@ function renderBarChart(daily: CostUsageDailyEntry[], metric: "totalTokens" | "t return html`
- ${daily.map((entry, i) => { + ${daily.map((entry) => { const value = entry[metric]; const height = Math.max(2, (value / max) * 150); const label = metric === "totalCost" ? formatCost(value) : formatNumber(value); @@ -76,9 +115,59 @@ function renderBarChart(daily: CostUsageDailyEntry[], metric: "totalTokens" | "t `; } +function renderQuotaBar(percent: number) { + const color = percent >= 90 ? "var(--danger)" : percent >= 70 ? "var(--warning)" : "var(--accent)"; + return html` +
+
+
+ `; +} + +function renderProviderQuota(provider: ProviderQuota) { + if (provider.error) { + return html` +
+
${provider.displayName}
+
${provider.error}
+
+ `; + } + + if (!provider.windows.length) { + return nothing; + } + + return html` +
+
+
${provider.displayName}
+ ${provider.plan ? html`
${provider.plan}
` : nothing} +
+
+ ${provider.windows.map( + (w) => html` +
+
+ ${w.label} + ${formatPercent(w.usedPercent)} used +
+ ${renderQuotaBar(w.usedPercent)} + ${w.resetAt ? html`
Resets in ${formatResetTime(w.resetAt)}
` : nothing} +
+ `, + )} +
+
+ `; +} + export function renderAnalytics(props: AnalyticsProps) { - const { loading, error, data, days } = props; + const { loading, error, data, quota, days } = props; const totals = data?.totals; + const hasQuota = quota && quota.some((p) => p.windows.length > 0); + const hasQuotaError = quota && quota.some((p) => p.error); + const quotaEmpty = quota && quota.length === 0; return html` -
+ ${hasQuota || hasQuotaError + ? html` +
+
Plan Quota
+
Current usage against your plan limits.
+
+ ${quota!.map((p) => renderProviderQuota(p))} +
+
+ ` + : quotaEmpty + ? html` +
+
Plan Quota
+
Current usage against your plan limits.
+
+ No plan quota data available. To see usage limits for Claude Max or other plans, + configure OAuth authentication with the required scopes. +
+
+ ` + : nothing} + +
Usage Summary
Token consumption and estimated costs.