refactor(ui): modularize auth-monitor into directory structure

- split auth-monitor.tsx (465 lines) into modular components
- extract provider-card, summary-card, live-pulse, inline-stats-badge
- separate hooks, types, and utils for better maintainability
- export types from monitoring barrel
This commit is contained in:
kaitranntt
2025-12-21 03:27:53 -05:00
parent 03d9bf76c4
commit 8ead6fa0bf
10 changed files with 576 additions and 465 deletions
@@ -1,465 +0,0 @@
/**
* 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, useEffect } from 'react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
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/shared/provider-icon';
import { AccountFlowViz } from '@/components/account-flow-viz';
import { usePrivacy } from '@/contexts/privacy-context';
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } 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 - darker for light theme contrast
const ACCOUNT_COLORS = [
'#1e6091', // Deep Cerulean (was #277da1)
'#2d8a6e', // Deep Seaweed (was #43aa8b)
'#d4a012', // Dark Tuscan (was #f9c74f)
'#c92a2d', // Deep Strawberry (was #f94144)
'#c45a1a', // Deep Pumpkin (was #f3722c)
'#6b9c4d', // Dark Willow (was #90be6d)
'#3d5a73', // Deep Blue Slate (was #577590)
'#cc7614', // Dark Carrot (was #f8961e)
'#3a7371', // Deep Cyan (was #4d908e)
'#7c5fc4', // Deep Purple (was #a78bfa)
];
/** Enhanced live pulse indicator with multi-ring animation */
function LivePulse() {
return (
<div className="relative flex items-center justify-center w-5 h-5">
{/* Outer ping ring */}
<div
className="absolute w-4 h-4 rounded-full animate-ping opacity-20"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
{/* Middle pulse ring */}
<div
className="absolute w-3 h-3 rounded-full animate-pulse opacity-40"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
{/* Inner solid dot */}
<div
className="relative w-2 h-2 rounded-full z-10"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
</div>
);
}
/** Inline success/failure badge for provider cards */
function InlineStatsBadge({ success, failure }: { success: number; failure: number }) {
if (success === 0 && failure === 0) {
return <span className="text-[9px] text-muted-foreground/50 font-mono">no activity</span>;
}
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-0.5">
<CheckCircle2 className="w-3 h-3 text-emerald-700 dark:text-emerald-500" />
<span className="text-[10px] font-mono font-medium text-emerald-700 dark:text-emerald-500">
{success.toLocaleString()}
</span>
</div>
{failure > 0 && (
<div className="flex items-center gap-0.5">
<XCircle className="w-3 h-3 text-red-700 dark:text-red-500" />
<span className="text-[10px] font-mono font-medium text-red-700 dark:text-red-500">
{failure.toLocaleString()}
</span>
</div>
)}
</div>
);
}
export function AuthMonitor() {
const { data, isLoading, error } = useCliproxyAuth();
const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
const { privacyMode } = usePrivacy();
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
// Live countdown showing time since last data update
useEffect(() => {
if (!dataUpdatedAt) return;
const updateTime = () => {
const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000);
if (diff < 60) {
setTimeSinceUpdate(`${diff}s ago`);
} else {
setTimeSinceUpdate(`${Math.floor(diff / 60)}m ago`);
}
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, [dataUpdatedAt]);
// Build a map of account email -> usage stats from CLIProxy
const accountStatsMap = useMemo(() => {
if (!statsData?.accountStats) return new Map<string, AccountUsageStats>();
return new Map(Object.entries(statsData.accountStats));
}, [statsData?.accountStats]);
// 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) => {
// Get real stats from CLIProxy - try email first, then id
const accountEmail = account.email || account.id;
const realStats = accountStatsMap.get(accountEmail);
const success = realStats?.successCount ?? 0;
const failure = realStats?.failureCount ?? 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: realStats?.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, accountStatsMap]);
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 || statsLoading) {
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-20" />
</div>
<div className="p-4 space-y-4">
<div className="flex gap-3">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-16 flex-1 rounded-lg" />
))}
</div>
<Skeleton className="h-48 w-full rounded-lg" />
</div>
</div>
);
}
if (error || !data?.authStatus || accounts.length === 0) {
return null;
}
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] text-foreground bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
{/* Enhanced Live Header with gradient glow */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-gradient-to-r from-emerald-500/5 via-transparent to-transparent dark:from-emerald-500/10">
<div className="flex items-center gap-2">
<LivePulse />
<span className="text-xs font-semibold tracking-tight text-foreground">LIVE</span>
<span className="text-[10px] text-muted-foreground">Account Monitor</span>
</div>
<div className="flex items-center gap-4 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5">
<Radio className="w-3 h-3 animate-pulse" />
<span>Updated {timeSinceUpdate || 'now'}</span>
</div>
<span className="text-muted-foreground/50">|</span>
<span>{accounts.length} accounts</span>
<span className="font-mono">{totalRequests.toLocaleString()} req</span>
</div>
</div>
{/* Summary Stats Row */}
<div className="grid grid-cols-4 gap-3 p-4 border-b border-border bg-muted/20 dark:bg-zinc-900/30">
<SummaryCard
icon={<Activity className="w-4 h-4" />}
label="Accounts"
value={accounts.length}
color="var(--accent)"
/>
<SummaryCard
icon={<CheckCircle2 className="w-4 h-4" />}
label="Success"
value={totalSuccess.toLocaleString()}
color={STATUS_COLORS.success}
/>
<SummaryCard
icon={<XCircle className="w-4 h-4" />}
label="Failed"
value={totalFailure.toLocaleString()}
color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
/>
<SummaryCard
icon={<Activity className="w-4 h-4" />}
label="Success Rate"
value={`${overallSuccessRate}%`}
color={
overallSuccessRate === 100
? STATUS_COLORS.success
: overallSuccessRate >= 95
? STATUS_COLORS.degraded
: STATUS_COLORS.failed
}
/>
</div>
{/* Flow Visualization */}
<div className="relative overflow-hidden">
{selectedProviderData ? (
// Account-level flow view
<AccountFlowViz
providerData={selectedProviderData}
onBack={() => setSelectedProvider(null)}
/>
) : (
// Provider cards view
<div className="p-6">
<div className="text-[10px] text-muted-foreground uppercase tracking-widest mb-4">
Request Distribution by Provider
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{providerStats.map((ps) => {
const successRate = getSuccessRate(ps.successCount, ps.failureCount);
const providerColor = PROVIDER_COLORS[ps.provider.toLowerCase()] || '#6b7280';
const isHovered = hoveredProvider === ps.provider;
return (
<button
key={ps.provider}
onClick={() => setSelectedProvider(ps.provider)}
onMouseEnter={() => setHoveredProvider(ps.provider)}
onMouseLeave={() => setHoveredProvider(null)}
className={cn(
'group relative rounded-xl p-4 text-left transition-all duration-300',
'bg-muted/30 dark:bg-zinc-900/60 backdrop-blur-sm',
'border border-border/50 dark:border-white/[0.08]',
'hover:border-opacity-50 hover:scale-[1.02] hover:shadow-lg',
isHovered && 'ring-1'
)}
style={
{
borderColor: isHovered ? providerColor : undefined,
'--ring-color': providerColor,
} as React.CSSProperties
}
>
<div className="flex items-center gap-3 mb-3">
<ProviderIcon provider={ps.provider} size={36} withBackground />
<div>
<h3 className="text-sm font-semibold text-foreground tracking-tight">
{ps.displayName}
</h3>
<p className="text-[10px] text-muted-foreground">
{ps.accountCount} account{ps.accountCount !== 1 ? 's' : ''}
</p>
</div>
<ChevronRight
className={cn(
'w-4 h-4 ml-auto text-muted-foreground transition-all',
isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-2'
)}
/>
</div>
<div className="space-y-2">
{/* Inline success/failure stats - immediately visible */}
<div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">Stats</span>
<InlineStatsBadge success={ps.successCount} failure={ps.failureCount} />
</div>
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">Success Rate</span>
<span
className="font-mono font-semibold"
style={{
color:
successRate === 100
? STATUS_COLORS.success
: successRate >= 95
? STATUS_COLORS.degraded
: STATUS_COLORS.failed,
}}
>
{successRate}%
</span>
</div>
{/* Progress bar */}
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${successRate}%`,
backgroundColor: providerColor,
}}
/>
</div>
</div>
{/* Account color dots */}
<div className="flex gap-1 mt-3">
{ps.accounts.slice(0, 5).map((acc) => (
<div
key={acc.id}
className="w-2 h-2 rounded-full"
style={{ backgroundColor: acc.color }}
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
/>
))}
{ps.accounts.length > 5 && (
<span className="text-[10px] text-muted-foreground ml-1">
+{ps.accounts.length - 5}
</span>
)}
</div>
</button>
);
})}
</div>
</div>
)}
</div>
</div>
);
}
// Summary Card Component
function SummaryCard({
icon,
label,
value,
color,
}: {
icon: React.ReactNode;
label: string;
value: string | number;
color?: string;
}) {
return (
<div className="flex items-center gap-3 p-3 rounded-lg bg-card/50 dark:bg-zinc-900/50 border border-border/50 dark:border-white/[0.06]">
<div
className="w-8 h-8 rounded-md flex items-center justify-center"
style={{
backgroundColor: color ? `${color}15` : 'var(--muted)',
color: color || 'var(--muted-foreground)',
}}
>
{icon}
</div>
<div>
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">{label}</div>
<div
className="text-lg font-semibold font-mono leading-tight"
style={{ color: color || 'var(--foreground)' }}
>
{value}
</div>
</div>
</div>
);
}
@@ -0,0 +1,35 @@
/**
* InlineStatsBadge - Inline success/failure badge for provider cards
*/
import { CheckCircle2, XCircle } from 'lucide-react';
interface InlineStatsBadgeProps {
success: number;
failure: number;
}
export function InlineStatsBadge({ success, failure }: InlineStatsBadgeProps) {
if (success === 0 && failure === 0) {
return <span className="text-[9px] text-muted-foreground/50 font-mono">no activity</span>;
}
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-0.5">
<CheckCircle2 className="w-3 h-3 text-emerald-700 dark:text-emerald-500" />
<span className="text-[10px] font-mono font-medium text-emerald-700 dark:text-emerald-500">
{success.toLocaleString()}
</span>
</div>
{failure > 0 && (
<div className="flex items-center gap-0.5">
<XCircle className="w-3 h-3 text-red-700 dark:text-red-500" />
<span className="text-[10px] font-mono font-medium text-red-700 dark:text-red-500">
{failure.toLocaleString()}
</span>
</div>
)}
</div>
);
}
@@ -0,0 +1,27 @@
/**
* LivePulse - Enhanced live pulse indicator with multi-ring animation
*/
import { STATUS_COLORS } from '@/lib/utils';
export function LivePulse() {
return (
<div className="relative flex items-center justify-center w-5 h-5">
{/* Outer ping ring */}
<div
className="absolute w-4 h-4 rounded-full animate-ping opacity-20"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
{/* Middle pulse ring */}
<div
className="absolute w-3 h-3 rounded-full animate-pulse opacity-40"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
{/* Inner solid dot */}
<div
className="relative w-2 h-2 rounded-full z-10"
style={{ backgroundColor: STATUS_COLORS.success }}
/>
</div>
);
}
@@ -0,0 +1,123 @@
/**
* ProviderCard - Provider status card with account color dots and stats
*/
import type React from 'react';
import { ChevronRight } from 'lucide-react';
import { cn, STATUS_COLORS } from '@/lib/utils';
import { PROVIDER_COLORS } from '@/lib/provider-config';
import { ProviderIcon } from '@/components/shared/provider-icon';
import type { ProviderStats } from '../types';
import { getSuccessRate, cleanEmail } from '../utils';
import { InlineStatsBadge } from './inline-stats-badge';
interface ProviderCardProps {
stats: ProviderStats;
isHovered: boolean;
privacyMode: boolean;
onSelect: () => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
export function ProviderCard({
stats,
isHovered,
privacyMode,
onSelect,
onMouseEnter,
onMouseLeave,
}: ProviderCardProps) {
const successRate = getSuccessRate(stats.successCount, stats.failureCount);
const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280';
return (
<button
onClick={onSelect}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={cn(
'group relative rounded-xl p-4 text-left transition-all duration-300',
'bg-muted/30 dark:bg-zinc-900/60 backdrop-blur-sm',
'border border-border/50 dark:border-white/[0.08]',
'hover:border-opacity-50 hover:scale-[1.02] hover:shadow-lg',
isHovered && 'ring-1'
)}
style={
{
borderColor: isHovered ? providerColor : undefined,
'--ring-color': providerColor,
} as React.CSSProperties
}
>
<div className="flex items-center gap-3 mb-3">
<ProviderIcon provider={stats.provider} size={36} withBackground />
<div>
<h3 className="text-sm font-semibold text-foreground tracking-tight">
{stats.displayName}
</h3>
<p className="text-[10px] text-muted-foreground">
{stats.accountCount} account{stats.accountCount !== 1 ? 's' : ''}
</p>
</div>
<ChevronRight
className={cn(
'w-4 h-4 ml-auto text-muted-foreground transition-all',
isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-2'
)}
/>
</div>
<div className="space-y-2">
{/* Inline success/failure stats - immediately visible */}
<div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">Stats</span>
<InlineStatsBadge success={stats.successCount} failure={stats.failureCount} />
</div>
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">Success Rate</span>
<span
className="font-mono font-semibold"
style={{
color:
successRate === 100
? STATUS_COLORS.success
: successRate >= 95
? STATUS_COLORS.degraded
: STATUS_COLORS.failed,
}}
>
{successRate}%
</span>
</div>
{/* Progress bar */}
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${successRate}%`,
backgroundColor: providerColor,
}}
/>
</div>
</div>
{/* Account color dots */}
<div className="flex gap-1 mt-3">
{stats.accounts.slice(0, 5).map((acc) => (
<div
key={acc.id}
className="w-2 h-2 rounded-full"
style={{ backgroundColor: acc.color }}
title={privacyMode ? '••••••' : cleanEmail(acc.email)}
/>
))}
{stats.accounts.length > 5 && (
<span className="text-[10px] text-muted-foreground ml-1">
+{stats.accounts.length - 5}
</span>
)}
</div>
</button>
);
}
@@ -0,0 +1,37 @@
/**
* SummaryCard - Summary stats card with icon and value display
*/
import type React from 'react';
interface SummaryCardProps {
icon: React.ReactNode;
label: string;
value: string | number;
color?: string;
}
export function SummaryCard({ icon, label, value, color }: SummaryCardProps) {
return (
<div className="flex items-center gap-3 p-3 rounded-lg bg-card/50 dark:bg-zinc-900/50 border border-border/50 dark:border-white/[0.06]">
<div
className="w-8 h-8 rounded-md flex items-center justify-center"
style={{
backgroundColor: color ? `${color}15` : 'var(--muted)',
color: color || 'var(--muted-foreground)',
}}
>
{icon}
</div>
<div>
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">{label}</div>
<div
className="text-lg font-semibold font-mono leading-tight"
style={{ color: color || 'var(--foreground)' }}
>
{value}
</div>
</div>
</div>
);
}
@@ -0,0 +1,148 @@
/**
* Hooks for Auth Monitor data aggregation and state management
*/
import { useState, useMemo, useEffect } from 'react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
import { getProviderDisplayName } from '@/lib/provider-config';
import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
import type { AccountRow, ProviderStats } from './types';
import { ACCOUNT_COLORS } from './utils';
export interface AuthMonitorData {
accounts: AccountRow[];
totalSuccess: number;
totalFailure: number;
totalRequests: number;
providerStats: ProviderStats[];
overallSuccessRate: number;
isLoading: boolean;
error: Error | null;
timeSinceUpdate: string;
}
/** Hook for computing auth monitor data from CLIProxy auth and stats */
export function useAuthMonitorData(): AuthMonitorData {
const { data, isLoading, error } = useCliproxyAuth();
const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
// Live countdown showing time since last data update
useEffect(() => {
if (!dataUpdatedAt) return;
const updateTime = () => {
const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000);
if (diff < 60) {
setTimeSinceUpdate(`${diff}s ago`);
} else {
setTimeSinceUpdate(`${Math.floor(diff / 60)}m ago`);
}
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, [dataUpdatedAt]);
// Build a map of account email -> usage stats from CLIProxy
const accountStatsMap = useMemo(() => {
if (!statsData?.accountStats) return new Map<string, AccountUsageStats>();
return new Map(Object.entries(statsData.accountStats));
}, [statsData?.accountStats]);
// Transform auth status data into account rows
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
if (!data?.authStatus) {
return {
accounts: [] as AccountRow[],
totalSuccess: 0,
totalFailure: 0,
totalRequests: 0,
providerStats: [] as 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) => {
const accountEmail = account.email || account.id;
const realStats = accountStatsMap.get(accountEmail);
const success = realStats?.successCount ?? 0;
const failure = realStats?.failureCount ?? 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: realStats?.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, accountStatsMap]);
const overallSuccessRate =
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
return {
accounts,
totalSuccess,
totalFailure,
totalRequests,
providerStats,
overallSuccessRate,
isLoading: isLoading || statsLoading,
error: error ?? null,
timeSinceUpdate,
};
}
@@ -0,0 +1,151 @@
/**
* 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 } from 'react';
import { STATUS_COLORS } from '@/lib/utils';
import { Skeleton } from '@/components/ui/skeleton';
import { AccountFlowViz } from '@/components/account-flow-viz';
import { usePrivacy } from '@/contexts/privacy-context';
import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react';
import { useAuthMonitorData } from './hooks';
import { LivePulse } from './components/live-pulse';
import { ProviderCard } from './components/provider-card';
import { SummaryCard } from './components/summary-card';
export function AuthMonitor() {
const {
accounts,
totalSuccess,
totalFailure,
totalRequests,
providerStats,
overallSuccessRate,
isLoading,
error,
timeSinceUpdate,
} = useAuthMonitorData();
const { privacyMode } = usePrivacy();
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
// Get selected provider data for detail view
const selectedProviderData = selectedProvider
? providerStats.find((ps) => ps.provider === selectedProvider)
: null;
if (isLoading) {
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-20" />
</div>
<div className="p-4 space-y-4">
<div className="flex gap-3">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-16 flex-1 rounded-lg" />
))}
</div>
<Skeleton className="h-48 w-full rounded-lg" />
</div>
</div>
);
}
if (error || accounts.length === 0) {
return null;
}
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] text-foreground bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
{/* Enhanced Live Header with gradient glow */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-gradient-to-r from-emerald-500/5 via-transparent to-transparent dark:from-emerald-500/10">
<div className="flex items-center gap-2">
<LivePulse />
<span className="text-xs font-semibold tracking-tight text-foreground">LIVE</span>
<span className="text-[10px] text-muted-foreground">Account Monitor</span>
</div>
<div className="flex items-center gap-4 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5">
<Radio className="w-3 h-3 animate-pulse" />
<span>Updated {timeSinceUpdate || 'now'}</span>
</div>
<span className="text-muted-foreground/50">|</span>
<span>{accounts.length} accounts</span>
<span className="font-mono">{totalRequests.toLocaleString()} req</span>
</div>
</div>
{/* Summary Stats Row */}
<div className="grid grid-cols-4 gap-3 p-4 border-b border-border bg-muted/20 dark:bg-zinc-900/30">
<SummaryCard
icon={<Activity className="w-4 h-4" />}
label="Accounts"
value={accounts.length}
color="var(--accent)"
/>
<SummaryCard
icon={<CheckCircle2 className="w-4 h-4" />}
label="Success"
value={totalSuccess.toLocaleString()}
color={STATUS_COLORS.success}
/>
<SummaryCard
icon={<XCircle className="w-4 h-4" />}
label="Failed"
value={totalFailure.toLocaleString()}
color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
/>
<SummaryCard
icon={<Activity className="w-4 h-4" />}
label="Success Rate"
value={`${overallSuccessRate}%`}
color={
overallSuccessRate === 100
? STATUS_COLORS.success
: overallSuccessRate >= 95
? STATUS_COLORS.degraded
: STATUS_COLORS.failed
}
/>
</div>
{/* Flow Visualization */}
<div className="relative overflow-hidden">
{selectedProviderData ? (
<AccountFlowViz
providerData={selectedProviderData}
onBack={() => setSelectedProvider(null)}
/>
) : (
<div className="p-6">
<div className="text-[10px] text-muted-foreground uppercase tracking-widest mb-4">
Request Distribution by Provider
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{providerStats.map((ps) => (
<ProviderCard
key={ps.provider}
stats={ps}
isHovered={hoveredProvider === ps.provider}
privacyMode={privacyMode}
onSelect={() => setSelectedProvider(ps.provider)}
onMouseEnter={() => setHoveredProvider(ps.provider)}
onMouseLeave={() => setHoveredProvider(null)}
/>
))}
</div>
</div>
)}
</div>
</div>
);
}
// Re-export types for barrel
export type { AccountRow, ProviderStats } from './types';
@@ -0,0 +1,25 @@
/**
* Type definitions for Auth Monitor components
*/
export interface AccountRow {
id: string;
email: string;
provider: string;
displayName: string;
isDefault: boolean;
successCount: number;
failureCount: number;
lastUsedAt?: string;
color: string;
}
export interface ProviderStats {
provider: string;
displayName: string;
totalRequests: number;
successCount: number;
failureCount: number;
accountCount: number;
accounts: AccountRow[];
}
@@ -0,0 +1,29 @@
/**
* Utility functions and constants for Auth Monitor
*/
/** Calculate success rate percentage */
export 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 */
export function cleanEmail(email: string): string {
return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, '');
}
// Vibrant colors for account segments - darker for light theme contrast
export const ACCOUNT_COLORS = [
'#1e6091', // Deep Cerulean
'#2d8a6e', // Deep Seaweed
'#d4a012', // Dark Tuscan
'#c92a2d', // Deep Strawberry
'#c45a1a', // Deep Pumpkin
'#6b9c4d', // Dark Willow
'#3d5a73', // Deep Blue Slate
'#cc7614', // Dark Carrot
'#3a7371', // Deep Cyan
'#7c5fc4', // Deep Purple
];
+1
View File
@@ -4,6 +4,7 @@
// Main monitoring components
export { AuthMonitor } from './auth-monitor';
export type { AccountRow, ProviderStats } from './auth-monitor';
export { ProxyStatusWidget } from './proxy-status-widget';
// Error logs (from subdirectory)