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>
This commit is contained in:
Lukas May
2026-03-06 13:35:14 +01:00
parent b4baba67a5
commit a94e72ccbc
2 changed files with 125 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
import { CheckCircle2, XCircle, AlertTriangle, Trash2 } from "lucide-react";
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();
@@ -100,6 +102,7 @@ export function AccountCard({
account: AccountData;
onDelete?: (e: React.MouseEvent) => void;
}) {
const [updateCredOpen, setUpdateCredOpen] = useState(false);
const hasWarning = account.credentialsValid && !account.isExhausted && account.error;
const statusIcon = !account.credentialsValid ? (
@@ -123,6 +126,7 @@ export function AccountCard({
const usage = account.usage;
return (
<>
<Card>
<CardContent className="space-y-3 py-4">
{/* Header row */}
@@ -147,6 +151,17 @@ export function AccountCard({
<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"
@@ -220,5 +235,11 @@ export function AccountCard({
)}
</CardContent>
</Card>
<UpdateCredentialsDialog
open={updateCredOpen}
onOpenChange={setUpdateCredOpen}
account={{ id: account.id, email: account.email, provider: account.provider }}
/>
</>
);
}

View File

@@ -1,9 +1,13 @@
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import {
CheckCircle2,
XCircle,
RefreshCw,
Server,
Plus,
RotateCcw,
Loader2,
} from 'lucide-react'
import { toast } from 'sonner'
import { trpc } from '@/lib/trpc'
@@ -11,6 +15,8 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/Skeleton'
import { AccountCard } from '@/components/AccountCard'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import { AddAccountDialog } from '@/components/AddAccountDialog'
export const Route = createFileRoute('/settings/health')({
component: HealthCheckPage,
@@ -45,7 +51,23 @@ function HealthCheckPage() {
},
})
const { data, isLoading, isError, error, refetch } = healthQuery
const [addAccountOpen, setAddAccountOpen] = useState(false)
const refreshAccounts = trpc.refreshAccounts.useMutation({
onSuccess: (data) => {
const msg =
data.cleared === 0
? 'No expired flags to clear.'
: `Cleared ${data.cleared} expired exhaustion flag(s).`
toast.success(msg)
void utils.systemHealthCheck.invalidate()
},
onError: (err) => {
toast.error(`Failed to refresh: ${err.message}`)
},
})
const { data, isLoading, isError, refetch } = healthQuery
// Loading state
if (isLoading) {
@@ -61,24 +83,9 @@ function HealthCheckPage() {
)
}
// Error state
if (isError) {
return (
<div className="flex flex-col items-center justify-center gap-4 py-12">
<XCircle className="h-8 w-8 text-status-error-fg" />
<p className="text-sm text-status-error-fg">
Failed to load health check: {error?.message ?? 'Unknown error'}
</p>
<Button variant="outline" size="sm" onClick={() => void refetch()}>
Retry
</Button>
</div>
)
}
if (!data) return null
const { server, accounts, projects } = data
const server = data?.server
const accounts = data?.accounts ?? []
const projects = data?.projects ?? []
return (
<div className="space-y-6">
@@ -95,37 +102,76 @@ function HealthCheckPage() {
</div>
{/* Server Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-display text-lg">
<Server className="h-5 w-5" />
Server Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 text-status-success-dot" />
<div>
<p className="text-sm font-medium">Running</p>
<p className="text-xs text-muted-foreground">
Uptime: {formatUptime(server.uptime)}
{server.startedAt && (
<>
{' '}
&middot; Started{' '}
{new Date(server.startedAt).toLocaleString()}
</>
)}
</p>
{server && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-display text-lg">
<Server className="h-5 w-5" />
Server Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 text-status-success-dot" />
<div>
<p className="text-sm font-medium">Running</p>
<p className="text-xs text-muted-foreground">
Uptime: {formatUptime(server.uptime)}
{server.startedAt && (
<>
{' '}
&middot; Started{' '}
{new Date(server.startedAt).toLocaleString()}
</>
)}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
)}
{/* Accounts */}
<div className="space-y-3">
<h2 className="font-display text-lg font-semibold">Accounts</h2>
{accounts.length === 0 ? (
<div className="flex items-center justify-between">
<h2 className="font-display text-lg font-semibold">Accounts</h2>
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => refreshAccounts.mutate()}
disabled={refreshAccounts.isPending}
>
{refreshAccounts.isPending
? <Loader2 className="h-4 w-4 animate-spin" />
: <RotateCcw className="h-4 w-4" />}
<span className="sr-only">Refresh</span>
</Button>
</TooltipTrigger>
<TooltipContent>Clear expired exhaustion flags</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
size="sm"
onClick={() => setAddAccountOpen(true)}
>
<Plus className="mr-2 h-4 w-4" />
Add Account
</Button>
</div>
</div>
{isError ? (
<Card>
<CardContent className="py-6">
<p className="text-center text-sm text-status-error-fg">
Could not load account status. Retrying
</p>
</CardContent>
</Card>
) : accounts.length === 0 ? (
<Card>
<CardContent className="py-6">
<p className="text-center text-sm text-muted-foreground">
@@ -133,7 +179,7 @@ function HealthCheckPage() {
<code className="rounded bg-muted px-1 py-0.5 text-xs">
cw account add
</code>{' '}
to register one.
to register one, or click 'Add Account' above.
</p>
</CardContent>
</Card>
@@ -143,7 +189,14 @@ function HealthCheckPage() {
key={account.id}
account={account}
onDelete={(e) => {
if (e.shiftKey || window.confirm(`Remove account "${account.email}"?`)) {
if (
e.shiftKey ||
(account.activeAgentCount > 0
? window.confirm(
`This account has ${account.activeAgentCount} active agent(s). Deleting it will not stop those agents but they will lose their account association. Continue?`
)
: window.confirm(`Remove account "${account.email}"?`))
) {
removeAccount.mutate({ id: account.id })
}
}}
@@ -186,6 +239,8 @@ function HealthCheckPage() {
))
)}
</div>
<AddAccountDialog open={addAccountOpen} onOpenChange={setAddAccountOpen} />
</div>
)
}