- health.tsx: Add Account button + AddAccountDialog, Refresh button with tooltip and spinner calling refreshAccounts mutation, inline account error state instead of full-page error, updated empty state text, and active-agent warning on delete confirm - AccountCard.tsx: Show KeyRound button when credentials/token invalid, opens UpdateCredentialsDialog pre-populated with account identity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
246 lines
7.8 KiB
TypeScript
246 lines
7.8 KiB
TypeScript
import { useState } from "react";
|
|
import { CheckCircle2, XCircle, AlertTriangle, Trash2, KeyRound } from "lucide-react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { UpdateCredentialsDialog } from "./UpdateCredentialsDialog";
|
|
|
|
function formatResetTime(isoDate: string): string {
|
|
const now = Date.now();
|
|
const target = new Date(isoDate).getTime();
|
|
const diffMs = target - now;
|
|
if (diffMs <= 0) return "now";
|
|
|
|
const totalMinutes = Math.floor(diffMs / 60_000);
|
|
const totalHours = Math.floor(totalMinutes / 60);
|
|
const totalDays = Math.floor(totalHours / 24);
|
|
|
|
if (totalDays > 0) {
|
|
const remainingHours = totalHours - totalDays * 24;
|
|
return `in ${totalDays}d ${remainingHours}h`;
|
|
}
|
|
const remainingMinutes = totalMinutes - totalHours * 60;
|
|
return `in ${totalHours}h ${remainingMinutes}m`;
|
|
}
|
|
|
|
function capitalize(s: string): string {
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
function UsageBar({
|
|
label,
|
|
utilization,
|
|
resetsAt,
|
|
}: {
|
|
label: string;
|
|
utilization: number;
|
|
resetsAt: string | null;
|
|
}) {
|
|
const color =
|
|
utilization >= 90
|
|
? "bg-status-error-dot"
|
|
: utilization >= 70
|
|
? "bg-status-warning-dot"
|
|
: "bg-status-success-dot";
|
|
const resetText = resetsAt ? formatResetTime(resetsAt) : null;
|
|
return (
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<span className="w-20 shrink-0 text-muted-foreground">{label}</span>
|
|
<div className="h-2 flex-1 rounded-full bg-muted">
|
|
<div
|
|
className={`h-2 rounded-full ${color}`}
|
|
style={{ width: `${Math.min(utilization, 100)}%` }}
|
|
/>
|
|
</div>
|
|
<span className="w-12 shrink-0 text-right">
|
|
{utilization.toFixed(0)}%
|
|
</span>
|
|
{resetText && (
|
|
<span className="shrink-0 text-muted-foreground">
|
|
resets {resetText}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type AccountData = {
|
|
id: string;
|
|
email: string;
|
|
provider: string;
|
|
credentialsValid: boolean;
|
|
tokenValid: boolean;
|
|
tokenExpiresAt: string | null;
|
|
subscriptionType: string | null;
|
|
error: string | null;
|
|
usage: {
|
|
five_hour: { utilization: number; resets_at: string | null } | null;
|
|
seven_day: { utilization: number; resets_at: string | null } | null;
|
|
seven_day_sonnet: {
|
|
utilization: number;
|
|
resets_at: string | null;
|
|
} | null;
|
|
seven_day_opus: { utilization: number; resets_at: string | null } | null;
|
|
extra_usage: {
|
|
is_enabled: boolean;
|
|
monthly_limit: number | null;
|
|
used_credits: number | null;
|
|
utilization: number | null;
|
|
} | null;
|
|
} | null;
|
|
isExhausted: boolean;
|
|
exhaustedUntil: string | null;
|
|
lastUsedAt: string | null;
|
|
agentCount: number;
|
|
activeAgentCount: number;
|
|
};
|
|
|
|
export function AccountCard({
|
|
account,
|
|
onDelete,
|
|
}: {
|
|
account: AccountData;
|
|
onDelete?: (e: React.MouseEvent) => void;
|
|
}) {
|
|
const [updateCredOpen, setUpdateCredOpen] = useState(false);
|
|
const hasWarning = account.credentialsValid && !account.isExhausted && account.error;
|
|
|
|
const statusIcon = !account.credentialsValid ? (
|
|
<XCircle className="h-5 w-5 shrink-0 text-status-error-fg" />
|
|
) : account.isExhausted ? (
|
|
<AlertTriangle className="h-5 w-5 shrink-0 text-status-warning-fg" />
|
|
) : hasWarning ? (
|
|
<AlertTriangle className="h-5 w-5 shrink-0 text-status-warning-fg" />
|
|
) : (
|
|
<CheckCircle2 className="h-5 w-5 shrink-0 text-status-success-fg" />
|
|
);
|
|
|
|
const statusText = !account.credentialsValid
|
|
? "Invalid credentials"
|
|
: account.isExhausted
|
|
? `Exhausted until ${account.exhaustedUntil ? new Date(account.exhaustedUntil).toLocaleTimeString() : "unknown"}`
|
|
: hasWarning
|
|
? "Setup incomplete"
|
|
: "Available";
|
|
|
|
const usage = account.usage;
|
|
|
|
return (
|
|
<>
|
|
<Card>
|
|
<CardContent className="space-y-3 py-4">
|
|
{/* Header row */}
|
|
<div className="flex items-start gap-3">
|
|
{statusIcon}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-medium">{account.email}</span>
|
|
<Badge variant="outline">{account.provider}</Badge>
|
|
{account.subscriptionType && (
|
|
<Badge variant="secondary">
|
|
{capitalize(account.subscriptionType)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
|
<span>
|
|
{account.agentCount} agent
|
|
{account.agentCount !== 1 ? "s" : ""} (
|
|
{account.activeAgentCount} active)
|
|
</span>
|
|
<span>{statusText}</span>
|
|
</div>
|
|
</div>
|
|
{(!account.credentialsValid || !account.tokenValid) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
onClick={() => setUpdateCredOpen(true)}
|
|
title="Update credentials"
|
|
>
|
|
<KeyRound className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
{onDelete && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="shrink-0 text-muted-foreground hover:text-destructive"
|
|
onClick={onDelete}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Usage bars */}
|
|
{usage && (
|
|
<div className="space-y-1.5 pl-8">
|
|
{usage.five_hour && (
|
|
<UsageBar
|
|
label="Session (5h)"
|
|
utilization={usage.five_hour.utilization}
|
|
resetsAt={usage.five_hour.resets_at}
|
|
/>
|
|
)}
|
|
{usage.seven_day && (
|
|
<UsageBar
|
|
label="Weekly (7d)"
|
|
utilization={usage.seven_day.utilization}
|
|
resetsAt={usage.seven_day.resets_at}
|
|
/>
|
|
)}
|
|
{usage.seven_day_sonnet &&
|
|
usage.seven_day_sonnet.utilization > 0 && (
|
|
<UsageBar
|
|
label="Sonnet (7d)"
|
|
utilization={usage.seven_day_sonnet.utilization}
|
|
resetsAt={usage.seven_day_sonnet.resets_at}
|
|
/>
|
|
)}
|
|
{usage.seven_day_opus && usage.seven_day_opus.utilization > 0 && (
|
|
<UsageBar
|
|
label="Opus (7d)"
|
|
utilization={usage.seven_day_opus.utilization}
|
|
resetsAt={usage.seven_day_opus.resets_at}
|
|
/>
|
|
)}
|
|
{usage.extra_usage && usage.extra_usage.is_enabled && (
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<span className="w-20 shrink-0 text-muted-foreground">
|
|
Extra usage
|
|
</span>
|
|
<span>
|
|
${((usage.extra_usage.used_credits ?? 0) / 100).toFixed(2)}{" "}
|
|
used
|
|
{usage.extra_usage.monthly_limit != null && (
|
|
<>
|
|
{" "}
|
|
/ $
|
|
{(usage.extra_usage.monthly_limit / 100).toFixed(2)} limit
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error / warning message */}
|
|
{account.error && (
|
|
<p className={`pl-8 text-xs ${hasWarning ? 'text-status-warning-fg' : 'text-destructive'}`}>
|
|
{account.error}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
<UpdateCredentialsDialog
|
|
open={updateCredOpen}
|
|
onOpenChange={setUpdateCredOpen}
|
|
account={{ id: account.id, email: account.email, provider: account.provider }}
|
|
/>
|
|
</>
|
|
);
|
|
}
|