[verified] feat: tighten dashboard capacity signals
This commit is contained in:
@@ -30,7 +30,14 @@ import {
|
|||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { clearToken, getToken, setToken } from '@/lib/storage';
|
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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -618,6 +625,9 @@ function Dashboard() {
|
|||||||
window: NonNullable<typeof usageWindows.primary>;
|
window: NonNullable<typeof usageWindows.primary>;
|
||||||
} => item.window !== null,
|
} => item.window !== null,
|
||||||
);
|
);
|
||||||
|
const fastestResetAt = getFastestResetAt(
|
||||||
|
windowCards.map((item) => item.window.resetAt),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen max-w-7xl px-4 py-5 sm:px-6 lg:px-8">
|
<div className="mx-auto min-h-screen max-w-7xl px-4 py-5 sm:px-6 lg:px-8">
|
||||||
@@ -657,9 +667,9 @@ function Dashboard() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Unified capacity</CardTitle>
|
<CardTitle>Unified capacity</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="flex h-full flex-col gap-4">
|
||||||
{windowCards.length > 0 ? (
|
{windowCards.length > 0 ? (
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid flex-1 gap-4 md:auto-rows-fr md:grid-cols-2">
|
||||||
{windowCards.map((item) => {
|
{windowCards.map((item) => {
|
||||||
const usedPercent = item.window.usedPercent;
|
const usedPercent = item.window.usedPercent;
|
||||||
const progressValue =
|
const progressValue =
|
||||||
@@ -670,7 +680,7 @@ function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.title}
|
key={item.title}
|
||||||
className="rounded-2xl border border-white/10 bg-white/4 p-4"
|
className="flex h-full flex-col justify-between rounded-2xl border border-white/10 bg-white/4 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -680,22 +690,18 @@ function Dashboard() {
|
|||||||
? `${usedPercent.toFixed(0)}%`
|
? `${usedPercent.toFixed(0)}%`
|
||||||
: '—'}
|
: '—'}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs uppercase tracking-[0.2em] text-slate-500">
|
|
||||||
{item.window.accountCount
|
|
||||||
? `Window data from ${item.window.accountCount} account${item.window.accountCount === 1 ? '' : 's'}`
|
|
||||||
: 'Used'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm text-slate-400">
|
<div className="text-right text-sm text-slate-400">
|
||||||
{item.window.limitWindowSeconds !== null ? (
|
{item.window.limitWindowSeconds !== null ? (
|
||||||
<div>{formatDurationSeconds(item.window.limitWindowSeconds)}</div>
|
<div>{formatDurationSeconds(item.window.limitWindowSeconds)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={item.window.limitWindowSeconds !== null ? 'mt-1' : ''}>
|
|
||||||
Resets {formatDate(item.window.resetAt)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Progress
|
||||||
<Progress value={progressValue} className="mt-4" />
|
value={progressValue}
|
||||||
|
className="mt-4"
|
||||||
|
indicatorClassName={getUsageProgressTone(progressValue)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -706,6 +712,9 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap gap-3 text-sm text-slate-400">
|
<div className="flex flex-wrap gap-3 text-sm text-slate-400">
|
||||||
|
{fastestResetAt ? (
|
||||||
|
<span>Replenishes at {formatDate(fastestResetAt)}</span>
|
||||||
|
) : null}
|
||||||
<span>Accounts: {summary.totals.totalAccounts}</span>
|
<span>Accounts: {summary.totals.totalAccounts}</span>
|
||||||
<span>Healthy: {summary.totals.activeAccounts}</span>
|
<span>Healthy: {summary.totals.activeAccounts}</span>
|
||||||
<span>Errors: {summary.totals.erroredAccounts}</span>
|
<span>Errors: {summary.totals.erroredAccounts}</span>
|
||||||
@@ -767,7 +776,17 @@ function Dashboard() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{summary.accounts.map((account) => (
|
{summary.accounts.map((account) => {
|
||||||
|
const accountWindows = extractUsageWindows(account.usage);
|
||||||
|
const accountCapacityBars = [
|
||||||
|
accountWindows.primary,
|
||||||
|
accountWindows.secondary,
|
||||||
|
].filter(
|
||||||
|
(window): window is NonNullable<typeof accountWindows.primary> =>
|
||||||
|
window !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<TabsContent key={account.id} value={account.id}>
|
<TabsContent key={account.id} value={account.id}>
|
||||||
<div className="space-y-4 rounded-3xl border border-white/10 bg-white/4 p-5">
|
<div className="space-y-4 rounded-3xl border border-white/10 bg-white/4 p-5">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
@@ -791,6 +810,31 @@ function Dashboard() {
|
|||||||
{account.status}
|
{account.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
{accountCapacityBars.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{accountCapacityBars.map((window, index) => {
|
||||||
|
const progressValue =
|
||||||
|
window.usedPercent !== null
|
||||||
|
? Math.max(0, Math.min(100, window.usedPercent))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`${account.id}-${index}`} className="flex items-center gap-2">
|
||||||
|
<Progress
|
||||||
|
value={progressValue}
|
||||||
|
className="h-1.5 flex-1"
|
||||||
|
indicatorClassName={getUsageProgressTone(progressValue)}
|
||||||
|
/>
|
||||||
|
<div className="min-w-10 text-right text-xs font-medium text-slate-400">
|
||||||
|
{window.usedPercent !== null
|
||||||
|
? `${window.usedPercent.toFixed(0)}%`
|
||||||
|
: '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="space-y-2 text-sm text-slate-300">
|
<div className="space-y-2 text-sm text-slate-300">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -833,7 +877,8 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -165,6 +165,22 @@ export function summarizeUsageWindows(values: Array<unknown>) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFastestResetAt(values: Array<string | null>) {
|
||||||
|
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) {
|
export function formatDurationSeconds(value: number | null) {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
return 'Unknown window';
|
return 'Unknown window';
|
||||||
|
|||||||
18
apps/web/test/dashboard-account-capacity.test.js
Normal file
18
apps/web/test/dashboard-account-capacity.test.js
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,11 @@ describe('dashboard unified capacity windows', () => {
|
|||||||
expect(appSource).toContain('Secondary window');
|
expect(appSource).toContain('Secondary window');
|
||||||
expect(appSource).toContain('const windowCards = [');
|
expect(appSource).toContain('const windowCards = [');
|
||||||
expect(appSource).toContain("} => item.window !== null,");
|
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('<CardTitle>Usage metrics</CardTitle>');
|
expect(appSource).not.toContain('<CardTitle>Usage metrics</CardTitle>');
|
||||||
expect(appSource).not.toContain('flattenNumericMetrics(summaryQuery.data?.aggregatedUsage).slice(0, 6)');
|
expect(appSource).not.toContain('flattenNumericMetrics(summaryQuery.data?.aggregatedUsage).slice(0, 6)');
|
||||||
expect(appSource).not.toContain('const firstMetric = metricCards[0]?.value ?? 0;');
|
expect(appSource).not.toContain('const firstMetric = metricCards[0]?.value ?? 0;');
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ describe('dashboard card copy', () => {
|
|||||||
expect(appSource).toContain('<CardTitle>Connected OpenAI accounts</CardTitle>');
|
expect(appSource).toContain('<CardTitle>Connected OpenAI accounts</CardTitle>');
|
||||||
expect(appSource).toContain('Primary window');
|
expect(appSource).toContain('Primary window');
|
||||||
expect(appSource).toContain('Secondary window');
|
expect(appSource).toContain('Secondary window');
|
||||||
|
expect(appSource).toContain('Replenishes at');
|
||||||
|
expect(appSource).not.toContain('Window data from');
|
||||||
expect(appSource).not.toContain('<CardTitle>Usage metrics</CardTitle>');
|
expect(appSource).not.toContain('<CardTitle>Usage metrics</CardTitle>');
|
||||||
expect(appSource).toContain(">Merged by default. Inspect each account below.<");
|
expect(appSource).toContain(">Merged by default. Inspect each account below.<");
|
||||||
expect(appSource).not.toContain(
|
expect(appSource).not.toContain(
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { join } from 'node:path';
|
|||||||
const appSource = readFileSync(join(import.meta.dir, '../src/App.tsx'), 'utf8');
|
const appSource = readFileSync(join(import.meta.dir, '../src/App.tsx'), 'utf8');
|
||||||
|
|
||||||
describe('mobile overflow guards', () => {
|
describe('mobile overflow guards', () => {
|
||||||
test('unified capacity window cards stay in a responsive two-column grid', () => {
|
test('unified capacity window cards stay in a responsive full-height two-column grid', () => {
|
||||||
expect(appSource).toContain('className="grid gap-4 md:grid-cols-2"');
|
expect(appSource).toContain('className="grid flex-1 gap-4 md:auto-rows-fr md:grid-cols-2"');
|
||||||
expect(appSource).toContain('className="rounded-2xl border border-white/10 bg-white/4 p-4"');
|
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}</div>');
|
expect(appSource).toContain('className="text-sm text-slate-400">{item.title}</div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { describe, expect, test } from 'bun:test';
|
|||||||
import {
|
import {
|
||||||
extractUsageWindows,
|
extractUsageWindows,
|
||||||
formatDurationSeconds,
|
formatDurationSeconds,
|
||||||
|
getFastestResetAt,
|
||||||
|
getUsageProgressTone,
|
||||||
summarizeUsageWindows,
|
summarizeUsageWindows,
|
||||||
} from '../src/lib/utils';
|
} from '../src/lib/utils';
|
||||||
|
|
||||||
@@ -42,6 +44,22 @@ describe('usage window helpers', () => {
|
|||||||
expect(formatDurationSeconds(null)).toBe('Unknown window');
|
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', () => {
|
test('summarizes windows across multiple account payloads without summing percentages', () => {
|
||||||
const windows = summarizeUsageWindows([
|
const windows = summarizeUsageWindows([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user