diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 44b52e8..4d7d39d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -30,7 +30,14 @@ import { import { toast, Toaster } from 'sonner'; import { api } from '@/lib/api'; import { clearToken, getToken, setToken } from '@/lib/storage'; -import { summarizeUsageWindows, formatDate, formatDurationSeconds } from '@/lib/utils'; +import { + extractUsageWindows, + formatDate, + formatDurationSeconds, + getFastestResetAt, + getUsageProgressTone, + summarizeUsageWindows, +} from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Card, @@ -618,6 +625,9 @@ function Dashboard() { window: NonNullable; } => item.window !== null, ); + const fastestResetAt = getFastestResetAt( + windowCards.map((item) => item.window.resetAt), + ); return (
@@ -657,9 +667,9 @@ function Dashboard() { Unified capacity - + {windowCards.length > 0 ? ( -
+
{windowCards.map((item) => { const usedPercent = item.window.usedPercent; const progressValue = @@ -670,7 +680,7 @@ function Dashboard() { return (
@@ -680,22 +690,18 @@ function Dashboard() { ? `${usedPercent.toFixed(0)}%` : '—'}
-
- {item.window.accountCount - ? `Window data from ${item.window.accountCount} account${item.window.accountCount === 1 ? '' : 's'}` - : 'Used'} -
{item.window.limitWindowSeconds !== null ? (
{formatDurationSeconds(item.window.limitWindowSeconds)}
) : null} -
- Resets {formatDate(item.window.resetAt)} -
- +
); })} @@ -706,6 +712,9 @@ function Dashboard() {
)}
+ {fastestResetAt ? ( + Replenishes at {formatDate(fastestResetAt)} + ) : null} Accounts: {summary.totals.totalAccounts} Healthy: {summary.totals.activeAccounts} Errors: {summary.totals.erroredAccounts} @@ -767,7 +776,17 @@ function Dashboard() { ))} - {summary.accounts.map((account) => ( + {summary.accounts.map((account) => { + const accountWindows = extractUsageWindows(account.usage); + const accountCapacityBars = [ + accountWindows.primary, + accountWindows.secondary, + ].filter( + (window): window is NonNullable => + window !== null, + ); + + return (
@@ -791,6 +810,31 @@ function Dashboard() { {account.status}
+ {accountCapacityBars.length > 0 ? ( +
+ {accountCapacityBars.map((window, index) => { + const progressValue = + window.usedPercent !== null + ? Math.max(0, Math.min(100, window.usedPercent)) + : 0; + + return ( +
+ +
+ {window.usedPercent !== null + ? `${window.usedPercent.toFixed(0)}%` + : '—'} +
+
+ ); + })} +
+ ) : null}
@@ -833,7 +877,8 @@ function Dashboard() {
- ))} + ); + })} )} diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index 3f6df84..68fec5c 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -165,6 +165,22 @@ export function summarizeUsageWindows(values: Array) { }; } +export function getFastestResetAt(values: Array) { + return earliestDate(values); +} + +export function getUsageProgressTone(value: number) { + if (value >= 80) { + return 'from-rose-500 to-red-400'; + } + + if (value >= 50) { + return 'from-amber-400 to-orange-300'; + } + + return 'from-sky-400 to-cyan-300'; +} + export function formatDurationSeconds(value: number | null) { if (!value || value <= 0) { return 'Unknown window'; diff --git a/apps/web/test/dashboard-account-capacity.test.js b/apps/web/test/dashboard-account-capacity.test.js new file mode 100644 index 0000000..853d5e4 --- /dev/null +++ b/apps/web/test/dashboard-account-capacity.test.js @@ -0,0 +1,18 @@ +import { describe, expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const appSource = readFileSync(join(import.meta.dir, '../src/App.tsx'), 'utf8'); + +describe('dashboard account capacity bars', () => { + test('renders compact per-account capacity bars without verbose labels', () => { + expect(appSource).toContain('const accountWindows = extractUsageWindows(account.usage);'); + expect(appSource).toContain('const accountCapacityBars = ['); + expect(appSource).toContain("className=\"space-y-2\""); + expect(appSource).toContain('indicatorClassName={getUsageProgressTone(progressValue)}'); + expect(appSource).toContain("className=\"min-w-10 text-right text-xs font-medium text-slate-400\""); + expect(appSource).not.toContain('Account capacity'); + expect(appSource).not.toContain('Primary window used'); + expect(appSource).not.toContain('Secondary window used'); + }); +}); diff --git a/apps/web/test/dashboard-capacity-windows.test.js b/apps/web/test/dashboard-capacity-windows.test.js index 0be1c3d..522464e 100644 --- a/apps/web/test/dashboard-capacity-windows.test.js +++ b/apps/web/test/dashboard-capacity-windows.test.js @@ -11,7 +11,11 @@ describe('dashboard unified capacity windows', () => { expect(appSource).toContain('Secondary window'); expect(appSource).toContain('const windowCards = ['); expect(appSource).toContain("} => item.window !== null,"); - expect(appSource).toContain('item.window.limitWindowSeconds !== null ? ('); + expect(appSource).toContain('getUsageProgressTone(progressValue)'); + expect(appSource).toContain('const fastestResetAt = getFastestResetAt('); + expect(appSource).toContain('Replenishes at'); + expect(appSource).not.toContain('Window data from'); + expect(appSource).not.toContain('Resets '); expect(appSource).not.toContain('Usage metrics'); expect(appSource).not.toContain('flattenNumericMetrics(summaryQuery.data?.aggregatedUsage).slice(0, 6)'); expect(appSource).not.toContain('const firstMetric = metricCards[0]?.value ?? 0;'); diff --git a/apps/web/test/dashboard-card-copy.test.js b/apps/web/test/dashboard-card-copy.test.js index 38dfad9..ca7330b 100644 --- a/apps/web/test/dashboard-card-copy.test.js +++ b/apps/web/test/dashboard-card-copy.test.js @@ -10,6 +10,8 @@ describe('dashboard card copy', () => { expect(appSource).toContain('Connected OpenAI accounts'); expect(appSource).toContain('Primary window'); expect(appSource).toContain('Secondary window'); + expect(appSource).toContain('Replenishes at'); + expect(appSource).not.toContain('Window data from'); expect(appSource).not.toContain('Usage metrics'); expect(appSource).toContain(">Merged by default. Inspect each account below.<"); expect(appSource).not.toContain( diff --git a/apps/web/test/mobile-overflow-guards.test.js b/apps/web/test/mobile-overflow-guards.test.js index 762399d..af61d5b 100644 --- a/apps/web/test/mobile-overflow-guards.test.js +++ b/apps/web/test/mobile-overflow-guards.test.js @@ -5,9 +5,9 @@ import { join } from 'node:path'; const appSource = readFileSync(join(import.meta.dir, '../src/App.tsx'), 'utf8'); describe('mobile overflow guards', () => { - test('unified capacity window cards stay in a responsive two-column grid', () => { - expect(appSource).toContain('className="grid gap-4 md:grid-cols-2"'); - expect(appSource).toContain('className="rounded-2xl border border-white/10 bg-white/4 p-4"'); + test('unified capacity window cards stay in a responsive full-height two-column grid', () => { + expect(appSource).toContain('className="grid flex-1 gap-4 md:auto-rows-fr md:grid-cols-2"'); + expect(appSource).toContain('className="flex h-full flex-col justify-between rounded-2xl border border-white/10 bg-white/4 p-4"'); expect(appSource).toContain('className="text-sm text-slate-400">{item.title}
'); }); diff --git a/apps/web/test/usage-window-helpers.test.js b/apps/web/test/usage-window-helpers.test.js index c2a4e1c..35c480a 100644 --- a/apps/web/test/usage-window-helpers.test.js +++ b/apps/web/test/usage-window-helpers.test.js @@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test'; import { extractUsageWindows, formatDurationSeconds, + getFastestResetAt, + getUsageProgressTone, summarizeUsageWindows, } from '../src/lib/utils'; @@ -42,6 +44,22 @@ describe('usage window helpers', () => { expect(formatDurationSeconds(null)).toBe('Unknown window'); }); + test('maps usage thresholds to warning progress colors', () => { + expect(getUsageProgressTone(49)).toContain('from-sky-400'); + expect(getUsageProgressTone(50)).toContain('from-amber-400'); + expect(getUsageProgressTone(80)).toContain('from-rose-500'); + }); + + test('picks the fastest reset time across available windows', () => { + expect( + getFastestResetAt([ + '2026-05-09T12:00:00.000Z', + null, + '2026-05-09T10:00:00.000Z', + ]), + ).toBe('2026-05-09T10:00:00.000Z'); + }); + test('summarizes windows across multiple account payloads without summing percentages', () => { const windows = summarizeUsageWindows([ {