Files
Codewalkers/apps/web/src/components/AccountCard.tsx
Lukas May a94e72ccbc feat: Wire AddAccountDialog and UpdateCredentialsDialog into health page and AccountCard
- 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>
2026-03-06 13:35:14 +01:00

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 }}
/>
</>
);
}