[verified] fix: separate account capacity panel

This commit is contained in:
2026-05-09 17:21:22 +09:00
parent bfa8bb69f5
commit 1a6deae48d
5 changed files with 101 additions and 50 deletions

View File

@@ -628,6 +628,17 @@ function Dashboard() {
const fastestResetAt = getFastestResetAt( const fastestResetAt = getFastestResetAt(
windowCards.map((item) => item.window.resetAt), windowCards.map((item) => item.window.resetAt),
); );
const accountCapacityItems = 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 {
account,
accountCapacityBars,
};
});
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">
@@ -663,13 +674,13 @@ function Dashboard() {
</div> </div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(16rem,1fr)] xl:items-stretch"> <div className="grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(16rem,1fr)] xl:items-stretch">
<Card className="h-full"> <Card className="flex h-full flex-col overflow-hidden">
<CardHeader> <CardHeader>
<CardTitle>Unified capacity</CardTitle> <CardTitle>Unified capacity</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex h-full flex-col gap-4"> <CardContent className="flex flex-1 flex-col gap-4">
{windowCards.length > 0 ? ( {windowCards.length > 0 ? (
<div className="grid flex-1 gap-4 md:auto-rows-fr md:grid-cols-2"> <div className="grid flex-1 items-stretch gap-4 md:grid-cols-2">
{windowCards.map((item) => { {windowCards.map((item) => {
const usedPercent = item.window.usedPercent; const usedPercent = item.window.usedPercent;
const progressValue = const progressValue =
@@ -680,7 +691,7 @@ function Dashboard() {
return ( return (
<div <div
key={item.title} key={item.title}
className="flex h-full flex-col justify-between rounded-2xl border border-white/10 bg-white/4 p-4" className="flex min-h-0 flex-1 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>
@@ -695,6 +706,9 @@ function Dashboard() {
{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' : ''}>
Replenishes at {formatDate(item.window.resetAt)}
</div>
</div> </div>
</div> </div>
<Progress <Progress
@@ -713,7 +727,7 @@ function Dashboard() {
)} )}
<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 ? ( {fastestResetAt ? (
<span>Replenishes at {formatDate(fastestResetAt)}</span> <span>Earliest replenishment {formatDate(fastestResetAt)}</span>
) : null} ) : 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>
@@ -776,17 +790,7 @@ 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">
@@ -810,31 +814,6 @@ 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">
@@ -877,9 +856,76 @@ function Dashboard() {
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
))}
</Tabs>
)}
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Per-account capacity</CardTitle>
<CardDescription>Compact rate-limit bars for each connected OpenAI account.</CardDescription>
</CardHeader>
<CardContent>
{accountCapacityItems.length === 0 ? (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/3 p-6 text-sm text-slate-400">
No account capacity data yet.
</div>
) : (
<div className="space-y-3">
{accountCapacityItems.map(({ account, accountCapacityBars }) => (
<div
key={account.id}
className="space-y-2 rounded-2xl border border-white/10 bg-white/4 p-4"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-white">{account.label}</div>
<div className="truncate text-xs text-slate-400">
{account.providerEmail || account.emailHint || 'No email available yet'}
</div>
</div>
<Badge
className={
account.status === 'active'
? 'border-emerald-400/20 bg-emerald-400/10 text-emerald-200'
: 'border-rose-400/20 bg-rose-400/10 text-rose-200'
}
>
{account.status}
</Badge>
</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>
); );
})} })}
</Tabs> </div>
) : (
<div className="text-xs text-slate-500">No window data yet.</div>
)}
</div>
))}
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -5,13 +5,14 @@ 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('dashboard account capacity bars', () => { describe('dashboard account capacity bars', () => {
test('renders compact per-account capacity bars without verbose labels', () => { test('renders compact per-account capacity bars in a dedicated panel', () => {
expect(appSource).toContain('<CardTitle>Per-account capacity</CardTitle>');
expect(appSource).toContain('const accountWindows = extractUsageWindows(account.usage);'); expect(appSource).toContain('const accountWindows = extractUsageWindows(account.usage);');
expect(appSource).toContain('const accountCapacityBars = ['); expect(appSource).toContain('const accountCapacityBars = [');
expect(appSource).toContain("className=\"space-y-2\""); expect(appSource).toContain("className=\"space-y-3\"");
expect(appSource).toContain("className=\"space-y-2 rounded-2xl border border-white/10 bg-white/4 p-4\"");
expect(appSource).toContain('indicatorClassName={getUsageProgressTone(progressValue)}'); expect(appSource).toContain('indicatorClassName={getUsageProgressTone(progressValue)}');
expect(appSource).toContain("className=\"min-w-10 text-right text-xs font-medium text-slate-400\""); 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('Primary window used');
expect(appSource).not.toContain('Secondary window used'); expect(appSource).not.toContain('Secondary window used');
}); });

View File

@@ -14,6 +14,7 @@ describe('dashboard unified capacity windows', () => {
expect(appSource).toContain('getUsageProgressTone(progressValue)'); expect(appSource).toContain('getUsageProgressTone(progressValue)');
expect(appSource).toContain('const fastestResetAt = getFastestResetAt('); expect(appSource).toContain('const fastestResetAt = getFastestResetAt(');
expect(appSource).toContain('Replenishes at'); expect(appSource).toContain('Replenishes at');
expect(appSource).not.toContain('Replenish after');
expect(appSource).not.toContain('Window data from'); expect(appSource).not.toContain('Window data from');
expect(appSource).not.toContain('Resets '); expect(appSource).not.toContain('Resets ');
expect(appSource).not.toContain('<CardTitle>Usage metrics</CardTitle>'); expect(appSource).not.toContain('<CardTitle>Usage metrics</CardTitle>');

View File

@@ -11,6 +11,7 @@ describe('dashboard card copy', () => {
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).toContain('Replenishes at');
expect(appSource).not.toContain('Replenish after');
expect(appSource).not.toContain('Window data from'); 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.<");

View File

@@ -5,10 +5,12 @@ 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 full-height two-column grid', () => { test('unified capacity window cards stay inside the card without h-full overflow', () => {
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 overflow-hidden"');
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="flex flex-1 flex-col gap-4"');
expect(appSource).toContain('className="text-sm text-slate-400">{item.title}</div>'); expect(appSource).toContain('className="grid flex-1 items-stretch gap-4 md:grid-cols-2"');
expect(appSource).toContain('className="flex min-h-0 flex-1 flex-col justify-between rounded-2xl border border-white/10 bg-white/4 p-4"');
expect(appSource).toContain('Replenishes at {formatDate(item.window.resetAt)}');
}); });
test('connected account tabs no longer render a side-by-side payload column', () => { test('connected account tabs no longer render a side-by-side payload column', () => {