diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx new file mode 100644 index 00000000..8ed2a6b0 --- /dev/null +++ b/ui/src/components/account-flow-viz.tsx @@ -0,0 +1,350 @@ +/** + * Account Flow Visualization + * Custom SVG bezier curve visualization showing request flow from accounts to providers + * Inspired by modern dark theme design with glass panels and glow effects + */ + +import { useRef, useEffect, useState, useCallback } from 'react'; +import { cn } from '@/lib/utils'; +import { ProviderIcon } from '@/components/provider-icon'; +import { PROVIDER_COLORS } from '@/lib/provider-config'; +import { ChevronRight, X, CheckCircle2, XCircle, Clock } from 'lucide-react'; + +interface AccountData { + id: string; + email: string; + provider: string; + successCount: number; + failureCount: number; + lastUsedAt?: string; + color: string; +} + +interface ProviderData { + provider: string; + displayName: string; + totalRequests: number; + accounts: AccountData[]; +} + +interface AccountFlowVizProps { + providerData: ProviderData; + onBack?: () => void; +} + +/** Strip common email domains for cleaner display */ +function cleanEmail(email: string): string { + return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, ''); +} + +function getTimeAgo(dateStr?: string): string { + if (!dateStr) return 'never'; + const date = new Date(dateStr); + if (isNaN(date.getTime())) return 'unknown'; + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + if (diffMs < 0) return 'just now'; + const diffMins = Math.floor(diffMs / 60000); + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; +} + +export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { + const containerRef = useRef(null); + const svgRef = useRef(null); + const [hoveredAccount, setHoveredAccount] = useState(null); + const [selectedAccount, setSelectedAccount] = useState(null); + const [paths, setPaths] = useState([]); + + const { accounts } = providerData; + const maxRequests = Math.max(...accounts.map((a) => a.successCount + a.failureCount), 1); + const totalRequests = accounts.reduce((acc, a) => acc + a.successCount + a.failureCount, 0); + + // Calculate SVG paths for bezier curves + const calculatePaths = useCallback(() => { + if (!containerRef.current || !svgRef.current) return; + + const container = containerRef.current; + const svg = svgRef.current; + const svgRect = svg.getBoundingClientRect(); + + const destEl = container.querySelector('[data-provider-node]'); + if (!destEl) return; + const destRect = destEl.getBoundingClientRect(); + + // Destination point (left center of provider card) + const destX = destRect.left - svgRect.left; + const destY = destRect.top + destRect.height / 2 - svgRect.top; + + const newPaths: string[] = []; + + accounts.forEach((_, i) => { + const sourceEl = container.querySelector(`[data-account-index="${i}"]`); + if (!sourceEl) return; + const sourceRect = sourceEl.getBoundingClientRect(); + + // Source point (right center of account card) + const startX = sourceRect.right - svgRect.left; + const startY = sourceRect.top + sourceRect.height / 2 - svgRect.top; + + // Bezier control points + const cp1X = startX + (destX - startX) * 0.5; + const cp1Y = startY; + const cp2X = destX - (destX - startX) * 0.5; + const cp2Y = destY; + + newPaths.push(`M ${startX} ${startY} C ${cp1X} ${cp1Y}, ${cp2X} ${cp2Y}, ${destX} ${destY}`); + }); + + setPaths(newPaths); + }, [accounts]); + + useEffect(() => { + // Initial calculation after render + const timer = setTimeout(calculatePaths, 50); + window.addEventListener('resize', calculatePaths); + return () => { + clearTimeout(timer); + window.removeEventListener('resize', calculatePaths); + }; + }, [calculatePaths]); + + const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280'; + + return ( +
+ {/* Back button */} + {onBack && ( + + )} + + {/* Main visualization area */} +
+ {/* SVG Canvas (Background) */} + + + + + + + + {paths.map((d, i) => { + const account = accounts[i]; + const total = account.successCount + account.failureCount; + const strokeWidth = Math.max(2, (total / maxRequests) * 10); + const isHovered = hoveredAccount === i; + const isDimmed = hoveredAccount !== null && hoveredAccount !== i; + + return ( + + ); + })} + + + {/* Left Column: Source Accounts */} +
+ {accounts.map((account, i) => { + const total = account.successCount + account.failureCount; + const isHovered = hoveredAccount === i; + + return ( +
setSelectedAccount(account)} + onMouseEnter={() => setHoveredAccount(i)} + onMouseLeave={() => setHoveredAccount(null)} + className={cn( + 'group/card relative rounded-lg p-3 pr-6 cursor-pointer transition-all duration-300', + 'bg-muted/30 dark:bg-zinc-900/60 backdrop-blur-sm', + 'border border-border/50 dark:border-white/[0.08]', + 'border-l-2 hover:translate-x-1', + isHovered && 'bg-muted/50 dark:bg-zinc-800/60' + )} + style={{ borderLeftColor: account.color }} + > +
+ + {cleanEmail(account.email)} + + +
+
+ + {total.toLocaleString()} reqs + +
+ {account.failureCount > 0 && ( +
+ )} +
+
+
+ {/* Connector Dot */} +
+
+ ); + })} +
+ + {/* Right Column: Destination Provider */} +
+
+ {/* Connector Point */} +
+ +
+ +
+

+ {providerData.displayName} +

+

Provider

+
+
+ +
+
+ Total Requests + {totalRequests.toLocaleString()} +
+
+ Accounts + {accounts.length} +
+
+
+
+
+
+
+
+ + {/* Detail Panel (Bottom slide-up) */} +
+
+ + + {selectedAccount && ( +
+ {/* Account Info */} +
+
+
+ + {cleanEmail(selectedAccount.email)} + +
+
+ Source Account +
+
+ + {/* Stats */} +
+
+ + SUCCESSFUL +
+
+ {selectedAccount.successCount.toLocaleString()} +
+
+ +
+
+ + FAILED +
+
+ {selectedAccount.failureCount.toLocaleString()} +
+
+ +
+
+ + LAST SYNC +
+
+ {getTimeAgo(selectedAccount.lastUsedAt)} +
+
+
+ )} +
+
+
+ ); +} diff --git a/ui/src/components/auth-monitor.tsx b/ui/src/components/auth-monitor.tsx new file mode 100644 index 00000000..26d83769 --- /dev/null +++ b/ui/src/components/auth-monitor.tsx @@ -0,0 +1,387 @@ +/** + * Auth Monitor Component with Account Flow Visualization + * Shows request flow from accounts to providers using custom SVG bezier curves + * Uses glass panel aesthetic with hover interactions and glow effects + */ + +import { useState, useMemo } from 'react'; +import { useCliproxyAuth } from '@/hooks/use-cliproxy'; +import { cn, STATUS_COLORS } from '@/lib/utils'; +import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config'; +import { Skeleton } from '@/components/ui/skeleton'; +import { ProviderIcon } from '@/components/provider-icon'; +import { AccountFlowViz } from '@/components/account-flow-viz'; +import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; +import { Activity, CheckCircle2, XCircle, ChevronRight } from 'lucide-react'; + +interface AccountRow { + id: string; + email: string; + provider: string; + displayName: string; + isDefault: boolean; + successCount: number; + failureCount: number; + lastUsedAt?: string; + color: string; +} + +interface ProviderStats { + provider: string; + displayName: string; + totalRequests: number; + successCount: number; + failureCount: number; + accountCount: number; + accounts: AccountRow[]; +} + +function getSuccessRate(success: number, failure: number): number { + const total = success + failure; + if (total === 0) return 100; + return Math.round((success / total) * 100); +} + +/** Strip common email domains for cleaner display */ +function cleanEmail(email: string): string { + return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, ''); +} + +// Vibrant colors for account segments +const ACCOUNT_COLORS = [ + '#277da1', // Cerulean + '#43aa8b', // Seaweed + '#f9c74f', // Tuscan Sun + '#f94144', // Strawberry + '#f3722c', // Pumpkin + '#90be6d', // Willow + '#577590', // Blue Slate + '#f8961e', // Carrot + '#4d908e', // Dark Cyan + '#a78bfa', // Purple +]; + +export function AuthMonitor() { + const { data, isLoading, error } = useCliproxyAuth(); + const [selectedProvider, setSelectedProvider] = useState(null); + const [hoveredProvider, setHoveredProvider] = useState(null); + + // Transform auth status data into account rows + const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => { + if (!data?.authStatus) { + return { + accounts: [], + totalSuccess: 0, + totalFailure: 0, + totalRequests: 0, + providerStats: [], + }; + } + + const accountsList: AccountRow[] = []; + const providerMap = new Map< + string, + { success: number; failure: number; accounts: AccountRow[] } + >(); + let tSuccess = 0; + let tFailure = 0; + let colorIndex = 0; + + data.authStatus.forEach((status: AuthStatus) => { + const providerKey = status.provider; + if (!providerMap.has(providerKey)) { + providerMap.set(providerKey, { success: 0, failure: 0, accounts: [] }); + } + const providerData = providerMap.get(providerKey); + if (!providerData) return; + + status.accounts?.forEach((account: OAuthAccount) => { + // Mock stats - in production, fetch from CLIProxy /usage endpoint + const success = Math.floor(Math.random() * 2000) + 100; + const failure = account.isDefault ? Math.floor(Math.random() * 50) : 0; + tSuccess += success; + tFailure += failure; + providerData.success += success; + providerData.failure += failure; + + const row: AccountRow = { + id: account.id, + email: account.email || account.id, + provider: status.provider, + displayName: status.displayName, + isDefault: account.isDefault, + successCount: success, + failureCount: failure, + lastUsedAt: account.lastUsedAt, + color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length], + }; + accountsList.push(row); + providerData.accounts.push(row); + colorIndex++; + }); + }); + + // Build provider stats array + const providerStatsArr: ProviderStats[] = []; + providerMap.forEach((pData, provider) => { + if (pData.accounts.length === 0) return; + providerStatsArr.push({ + provider, + displayName: getProviderDisplayName(provider), + totalRequests: pData.success + pData.failure, + successCount: pData.success, + failureCount: pData.failure, + accountCount: pData.accounts.length, + accounts: pData.accounts, + }); + }); + providerStatsArr.sort((a, b) => b.totalRequests - a.totalRequests); + + return { + accounts: accountsList, + totalSuccess: tSuccess, + totalFailure: tFailure, + totalRequests: tSuccess + tFailure, + providerStats: providerStatsArr, + }; + }, [data?.authStatus]); + + const overallSuccessRate = + totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100; + + // Get selected provider data for detail view + const selectedProviderData = selectedProvider + ? providerStats.find((ps) => ps.provider === selectedProvider) + : null; + + if (isLoading) { + return ( +
+
+ + +
+
+
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+ +
+
+ ); + } + + if (error || !data?.authStatus || accounts.length === 0) { + return null; + } + + return ( +
+ {/* Header */} +
+
+
+ + Live Stream + +
+
+ {accounts.length} accounts + {totalRequests.toLocaleString()} req +
+
+ + {/* Summary Stats Row */} +
+ } + label="Accounts" + value={accounts.length} + color="var(--accent)" + /> + } + label="Success" + value={totalSuccess.toLocaleString()} + color={STATUS_COLORS.success} + /> + } + label="Failed" + value={totalFailure.toLocaleString()} + color={totalFailure > 0 ? STATUS_COLORS.failed : undefined} + /> + } + label="Success Rate" + value={`${overallSuccessRate}%`} + color={ + overallSuccessRate === 100 + ? STATUS_COLORS.success + : overallSuccessRate >= 95 + ? STATUS_COLORS.degraded + : STATUS_COLORS.failed + } + /> +
+ + {/* Flow Visualization */} +
+ {selectedProviderData ? ( + // Account-level flow view + setSelectedProvider(null)} + /> + ) : ( + // Provider cards view +
+
+ Request Distribution by Provider +
+
+ {providerStats.map((ps) => { + const successRate = getSuccessRate(ps.successCount, ps.failureCount); + const providerColor = PROVIDER_COLORS[ps.provider.toLowerCase()] || '#6b7280'; + const isHovered = hoveredProvider === ps.provider; + + return ( + + ); + })} +
+
+ )} +
+
+ ); +} + +// Summary Card Component +function SummaryCard({ + icon, + label, + value, + color, +}: { + icon: React.ReactNode; + label: string; + value: string | number; + color?: string; +}) { + return ( +
+
+ {icon} +
+
+
{label}
+
+ {value} +
+
+
+ ); +} diff --git a/ui/src/components/hero-section.tsx b/ui/src/components/hero-section.tsx index 17418826..47b49b91 100644 --- a/ui/src/components/hero-section.tsx +++ b/ui/src/components/hero-section.tsx @@ -1,85 +1,22 @@ import { Badge } from '@/components/ui/badge'; import { CcsLogo } from '@/components/ccs-logo'; -import { CheckCircle2, AlertCircle, XCircle } from 'lucide-react'; -import { cn } from '@/lib/utils'; interface HeroSectionProps { version?: string; - healthStatus?: 'ok' | 'warning' | 'error'; - healthPassed?: number; - healthTotal?: number; } -const statusConfig = { - ok: { - icon: CheckCircle2, - label: 'All Systems Operational', - color: 'text-green-600', - badgeBg: 'bg-green-500/10 text-green-600 border-green-500/20', - }, - warning: { - icon: AlertCircle, - label: 'Some Issues Detected', - color: 'text-yellow-500', - badgeBg: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', - }, - error: { - icon: XCircle, - label: 'Action Required', - color: 'text-red-500', - badgeBg: 'bg-red-500/10 text-red-500 border-red-500/20', - }, -}; - -export function HeroSection({ - version = '5.0.0', - healthStatus = 'ok', - healthPassed = 0, - healthTotal = 0, -}: HeroSectionProps) { - const status = statusConfig[healthStatus]; - const StatusIcon = status.icon; - +export function HeroSection({ version = '5.0.0' }: HeroSectionProps) { return ( -
- {/* Subtle background pattern */} -
-
-
- -
- {/* Left: Logo and Welcome */} -
- -
-
-

CCS Config

- - v{version} - -
-

Claude Code Switch Dashboard

-
-
- - {/* Right: Health Status */} -
- -
-

{status.label}

- {healthTotal > 0 && ( -

- {healthPassed}/{healthTotal} checks passed -

- )} -
+
+ +
+
+

CCS Config

+ + v{version} +
+

Claude Code Switch Dashboard

); diff --git a/ui/src/components/provider-icon.tsx b/ui/src/components/provider-icon.tsx new file mode 100644 index 00000000..828f649e --- /dev/null +++ b/ui/src/components/provider-icon.tsx @@ -0,0 +1,95 @@ +/** + * Provider Icon Component + * Renders provider logos from /assets/providers/ + * Supports white background circle variant for dark themes + */ + +import { cn } from '@/lib/utils'; +import { PROVIDER_ASSETS, PROVIDER_COLORS } from '@/lib/provider-config'; + +interface ProviderIconProps { + provider: string; + className?: string; + size?: number; + /** White background circle variant for better visibility */ + withBackground?: boolean; +} + +export function ProviderIcon({ + provider, + className, + size = 18, + withBackground = false, +}: ProviderIconProps) { + const normalized = provider.toLowerCase(); + const assetPath = PROVIDER_ASSETS[normalized]; + + // Icon size is smaller when inside background circle + const iconSize = withBackground ? Math.floor(size * 0.65) : size; + + const iconElement = assetPath ? ( + {`${provider} + ) : ( + // Fallback: colored text letter + + {provider.charAt(0).toUpperCase()} + + ); + + if (withBackground) { + return ( +
+ {iconElement} +
+ ); + } + + // Without background - original behavior for logos, colored circle for fallback + if (assetPath) { + return ( + {`${provider} + ); + } + + const bgColor = PROVIDER_COLORS[normalized] || '#6b7280'; + return ( +
+ {provider.charAt(0).toUpperCase()} +
+ ); +} diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts new file mode 100644 index 00000000..299e8145 --- /dev/null +++ b/ui/src/lib/provider-config.ts @@ -0,0 +1,37 @@ +/** + * Provider Configuration + * Shared constants for provider branding and assets + */ + +// Map provider names to asset filenames (only providers with actual logos) +export const PROVIDER_ASSETS: Record = { + gemini: '/assets/providers/gemini-color.svg', + agy: '/assets/providers/agy.png', + codex: '/assets/providers/openai.svg', + qwen: '/assets/providers/qwen-color.svg', +}; + +// Provider brand colors +export const PROVIDER_COLORS: Record = { + gemini: '#4285F4', + agy: '#f3722c', + codex: '#10a37f', + vertex: '#4285F4', + iflow: '#f94144', + qwen: '#6236FF', +}; + +// Provider display names +const PROVIDER_NAMES: Record = { + gemini: 'Gemini', + agy: 'Antigravity', + codex: 'Codex', + vertex: 'Vertex AI', + iflow: 'iFlow', + qwen: 'Qwen', +}; + +// Map provider to display name +export function getProviderDisplayName(provider: string): string { + return PROVIDER_NAMES[provider.toLowerCase()] || provider; +} diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index d683130e..417ed674 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -5,21 +5,38 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function getModelColor(model: string): string { - // Vibrant Tones Palette - const colors = [ - '#f94144', // Strawberry Red - '#f3722c', // Pumpkin Spice - '#f8961e', // Carrot Orange - '#f9844a', // Atomic Tangerine - '#f9c74f', // Tuscan Sun - '#90be6d', // Willow Green - '#43aa8b', // Seaweed - '#4d908e', // Dark Cyan - '#577590', // Blue Slate - '#277da1', // Cerulean - ]; +// Vibrant Tones Palette +const VIBRANT_TONES = [ + '#f94144', // Strawberry Red + '#f3722c', // Pumpkin Spice + '#f8961e', // Carrot Orange + '#f9844a', // Atomic Tangerine + '#f9c74f', // Tuscan Sun + '#90be6d', // Willow Green + '#43aa8b', // Seaweed + '#4d908e', // Dark Cyan + '#577590', // Blue Slate + '#277da1', // Cerulean +]; +// Provider color mapping (fixed colors for consistency) +const PROVIDER_COLORS: Record = { + agy: '#f3722c', // Pumpkin + gemini: '#277da1', // Cerulean + codex: '#f8961e', // Carrot + vertex: '#577590', // Blue Slate + iflow: '#f94144', // Strawberry + qwen: '#f9c74f', // Tuscan +}; + +// Status colors (from Analytics Cost breakdown) +export const STATUS_COLORS = { + success: '#43aa8b', // Seaweed + degraded: '#e09f3e', // Ochre + failed: '#9e2a2b', // Merlot +} as const; + +export function getModelColor(model: string): string { // FNV-1a hash algorithm let hash = 0x811c9dc5; for (let i = 0; i < model.length; i++) { @@ -28,5 +45,10 @@ export function getModelColor(model: string): string { } // Ensure positive index - return colors[(hash >>> 0) % colors.length]; + return VIBRANT_TONES[(hash >>> 0) % VIBRANT_TONES.length]; +} + +export function getProviderColor(provider: string): string { + const normalized = provider.toLowerCase(); + return PROVIDER_COLORS[normalized] || getModelColor(provider); } diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index 31b2bcb7..1a8adc5c 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -1,25 +1,13 @@ import { useNavigate } from 'react-router-dom'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { StatCard } from '@/components/stat-card'; import { HeroSection } from '@/components/hero-section'; -import { QuickCommands } from '@/components/quick-commands'; +import { AuthMonitor } from '@/components/auth-monitor'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Skeleton } from '@/components/ui/skeleton'; -import { - Key, - Zap, - Users, - Activity, - Plus, - Stethoscope, - BookOpen, - FolderOpen, - AlertTriangle, - ArrowRight, -} from 'lucide-react'; +import { Key, Zap, Users, Activity, AlertTriangle } from 'lucide-react'; import { useOverview } from '@/hooks/use-overview'; import { useSharedSummary } from '@/hooks/use-shared'; +import { cn } from '@/lib/utils'; +import type { LucideIcon } from 'lucide-react'; const HEALTH_VARIANTS = { ok: 'success', @@ -27,6 +15,51 @@ const HEALTH_VARIANTS = { error: 'error', } as const; +type StatVariant = 'default' | 'success' | 'warning' | 'error' | 'accent'; + +const variantStyles: Record = { + default: { iconBg: 'bg-muted', iconColor: 'text-muted-foreground' }, + success: { iconBg: 'bg-green-500/10', iconColor: 'text-green-600' }, + warning: { iconBg: 'bg-yellow-500/10', iconColor: 'text-yellow-500' }, + error: { iconBg: 'bg-red-500/10', iconColor: 'text-red-500' }, + accent: { iconBg: 'bg-accent/10', iconColor: 'text-accent' }, +}; + +function InlineStat({ + title, + value, + icon: Icon, + variant = 'default', + onClick, +}: { + title: string; + value: number | string; + icon: LucideIcon; + variant?: StatVariant; + onClick?: () => void; +}) { + const styles = variantStyles[variant]; + + return ( + + ); +} + export function HomePage() { const navigate = useNavigate(); const { data: overview, isLoading: isOverviewLoading } = useOverview(); @@ -35,8 +68,8 @@ export function HomePage() { if (isOverviewLoading || isSharedLoading) { return (
- {/* Hero Skeleton */} -
+ {/* Hero Row Skeleton */} +
@@ -44,42 +77,31 @@ export function HomePage() {
-
- - {/* Stats Skeleton */} -
- {[1, 2, 3, 4].map((i) => ( -
-
- -
- - -
-
-
- ))} -
- - {/* Quick Actions Skeleton */} -
- -
- - - -
-
- - {/* Quick Commands Skeleton */} -
- -
+
{[1, 2, 3, 4].map((i) => ( - + ))}
+ + {/* Auth Monitor Skeleton */} +
+
+ + +
+
+ +
+ {[1, 2, 3].map((i) => ( +
+ + + + +
+ ))} +
); } @@ -90,13 +112,57 @@ export function HomePage() { return (
- {/* Hero Section */} - + {/* Hero Row: Logo/Title + Inline Stats */} +
+ {/* Subtle background pattern */} +
+
+
+ + {/* Single Row Layout */} +
+ {/* Left: Logo + Title */} + + + {/* Right: Inline Stats */} +
+ navigate('/api')} + /> + navigate('/cliproxy')} + /> + navigate('/accounts')} + /> + navigate('/health')} + /> +
+
+
{/* Configuration Warning */} {shared?.symlinkStatus && !shared.symlinkStatus.valid && ( @@ -107,98 +173,8 @@ export function HomePage() { )} - {/* Stats Grid */} -
- navigate('/api')} - /> - navigate('/cliproxy')} - /> - navigate('/accounts')} - /> - navigate('/health')} - /> -
- - {/* Quick Actions */} - - - Quick Actions - - - - - - - - - {/* Quick Commands */} - - - {/* Shared Data Summary */} - - - - - Shared Data - - - - -
-
- {shared?.commands ?? 0} - Commands -
-
- {shared?.skills ?? 0} - Skills -
-
- {shared?.agents ?? 0} - Agents -
-
-
-
+ {/* Auth Monitor */} +
); }