From 8ead6fa0bf05fc8d37563a618c473d7eae808920 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 21 Dec 2025 03:27:53 -0500 Subject: [PATCH] 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 --- ui/src/components/monitoring/auth-monitor.tsx | 465 ------------------ .../components/inline-stats-badge.tsx | 35 ++ .../auth-monitor/components/live-pulse.tsx | 27 + .../auth-monitor/components/provider-card.tsx | 123 +++++ .../auth-monitor/components/summary-card.tsx | 37 ++ .../monitoring/auth-monitor/hooks.ts | 148 ++++++ .../monitoring/auth-monitor/index.tsx | 151 ++++++ .../monitoring/auth-monitor/types.ts | 25 + .../monitoring/auth-monitor/utils.ts | 29 ++ ui/src/components/monitoring/index.ts | 1 + 10 files changed, 576 insertions(+), 465 deletions(-) delete mode 100644 ui/src/components/monitoring/auth-monitor.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/components/provider-card.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/components/summary-card.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/hooks.ts create mode 100644 ui/src/components/monitoring/auth-monitor/index.tsx create mode 100644 ui/src/components/monitoring/auth-monitor/types.ts create mode 100644 ui/src/components/monitoring/auth-monitor/utils.ts diff --git a/ui/src/components/monitoring/auth-monitor.tsx b/ui/src/components/monitoring/auth-monitor.tsx deleted file mode 100644 index 61cbaf8b..00000000 --- a/ui/src/components/monitoring/auth-monitor.tsx +++ /dev/null @@ -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 ( -
- {/* Outer ping ring */} -
- {/* Middle pulse ring */} -
- {/* Inner solid dot */} -
-
- ); -} - -/** Inline success/failure badge for provider cards */ -function InlineStatsBadge({ success, failure }: { success: number; failure: number }) { - if (success === 0 && failure === 0) { - return no activity; - } - - return ( -
-
- - - {success.toLocaleString()} - -
- {failure > 0 && ( -
- - - {failure.toLocaleString()} - -
- )} -
- ); -} - -export function AuthMonitor() { - const { data, isLoading, error } = useCliproxyAuth(); - const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats(); - const { privacyMode } = usePrivacy(); - const [selectedProvider, setSelectedProvider] = useState(null); - const [hoveredProvider, setHoveredProvider] = useState(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(); - 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 ( -
-
- - -
-
-
- {[1, 2, 3, 4].map((i) => ( - - ))} -
- -
-
- ); - } - - if (error || !data?.authStatus || accounts.length === 0) { - return null; - } - - return ( -
- {/* Enhanced Live Header with gradient glow */} -
-
- - LIVE - Account Monitor -
-
-
- - Updated {timeSinceUpdate || 'now'} -
- | - {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/monitoring/auth-monitor/components/inline-stats-badge.tsx b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx new file mode 100644 index 00000000..bd60db8f --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx @@ -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 no activity; + } + + return ( +
+
+ + + {success.toLocaleString()} + +
+ {failure > 0 && ( +
+ + + {failure.toLocaleString()} + +
+ )} +
+ ); +} diff --git a/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx b/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx new file mode 100644 index 00000000..adc006ad --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx @@ -0,0 +1,27 @@ +/** + * LivePulse - Enhanced live pulse indicator with multi-ring animation + */ + +import { STATUS_COLORS } from '@/lib/utils'; + +export function LivePulse() { + return ( +
+ {/* Outer ping ring */} +
+ {/* Middle pulse ring */} +
+ {/* Inner solid dot */} +
+
+ ); +} diff --git a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx new file mode 100644 index 00000000..d7faf5f9 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx @@ -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 ( + + ); +} diff --git a/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx b/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx new file mode 100644 index 00000000..380d5d03 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx @@ -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 ( +
+
+ {icon} +
+
+
{label}
+
+ {value} +
+
+
+ ); +} diff --git a/ui/src/components/monitoring/auth-monitor/hooks.ts b/ui/src/components/monitoring/auth-monitor/hooks.ts new file mode 100644 index 00000000..1640f037 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/hooks.ts @@ -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(); + 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, + }; +} diff --git a/ui/src/components/monitoring/auth-monitor/index.tsx b/ui/src/components/monitoring/auth-monitor/index.tsx new file mode 100644 index 00000000..d43e1d48 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/index.tsx @@ -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(null); + const [hoveredProvider, setHoveredProvider] = useState(null); + + // 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 || accounts.length === 0) { + return null; + } + + return ( +
+ {/* Enhanced Live Header with gradient glow */} +
+
+ + LIVE + Account Monitor +
+
+
+ + Updated {timeSinceUpdate || 'now'} +
+ | + {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 ? ( + setSelectedProvider(null)} + /> + ) : ( +
+
+ Request Distribution by Provider +
+
+ {providerStats.map((ps) => ( + setSelectedProvider(ps.provider)} + onMouseEnter={() => setHoveredProvider(ps.provider)} + onMouseLeave={() => setHoveredProvider(null)} + /> + ))} +
+
+ )} +
+
+ ); +} + +// Re-export types for barrel +export type { AccountRow, ProviderStats } from './types'; diff --git a/ui/src/components/monitoring/auth-monitor/types.ts b/ui/src/components/monitoring/auth-monitor/types.ts new file mode 100644 index 00000000..1eb61953 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/types.ts @@ -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[]; +} diff --git a/ui/src/components/monitoring/auth-monitor/utils.ts b/ui/src/components/monitoring/auth-monitor/utils.ts new file mode 100644 index 00000000..28272ac7 --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/utils.ts @@ -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 +]; diff --git a/ui/src/components/monitoring/index.ts b/ui/src/components/monitoring/index.ts index 3d66966d..4f9ac659 100644 --- a/ui/src/components/monitoring/index.ts +++ b/ui/src/components/monitoring/index.ts @@ -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)