diff --git a/ui/src/components/error-logs-monitor.tsx b/ui/src/components/error-logs-monitor.tsx
index eb704397..6974ec81 100644
--- a/ui/src/components/error-logs-monitor.tsx
+++ b/ui/src/components/error-logs-monitor.tsx
@@ -1,617 +1,18 @@
/**
- * Error Logs Monitor Component
- *
- * Displays CLIProxyAPI error logs with master-detail split view.
- * ETL: Parses raw logs into structured data for rich display.
- * - Left panel: Log list with status code, provider, endpoint, relative time
- * - Right panel: Tabbed view (Overview, Headers, Request, Response, Raw)
+ * Error Logs Monitor - Re-export from modular structure
+ * @deprecated Import from '@/components/monitoring/error-logs' directly
*/
-import { useState, useMemo, useRef, useEffect } from 'react';
-import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
-import { useCliproxyErrorLogs, useCliproxyErrorLogContent } from '@/hooks/use-cliproxy-stats';
-import { useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
-import { cn } from '@/lib/utils';
-import { Skeleton } from '@/components/ui/skeleton';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import { ProviderIcon } from '@/components/provider-icon';
-import { CopyButton } from '@/components/ui/copy-button';
-import {
- AlertTriangle,
- FileWarning,
- Clock,
- FileText,
- Terminal,
- Info,
- Code,
- ArrowUpRight,
- ArrowDownLeft,
- GripVertical,
- GripHorizontal,
-} from 'lucide-react';
-import {
- parseErrorLog,
- parseFilename,
- formatRelativeTime,
- formatBytes,
- getStatusColor,
- getErrorTypeLabel,
- type ParsedErrorLog,
-} from '@/lib/error-log-parser';
+export {
+ ErrorLogsMonitor,
+ ErrorLogItem,
+ LogContentPanel,
+ TabButton,
+ StatusBadge,
+ OverviewTab,
+ HeadersTab,
+ BodyTab,
+ RawTab,
+} from './monitoring/error-logs';
-type TabType = 'overview' | 'headers' | 'request' | 'response' | 'raw';
-
-/** Tab button component */
-function TabButton({
- active,
- onClick,
- children,
- icon: Icon,
-}: {
- active: boolean;
- onClick: () => void;
- children: React.ReactNode;
- icon?: React.ComponentType<{ className?: string }>;
-}) {
- return (
-
- );
-}
-
-/** Status badge component */
-function StatusBadge({ code }: { code: number }) {
- const colorClass = getStatusColor(code);
- return (
-
- {code}
-
- );
-}
-
-/** Overview tab content */
-function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
- return (
-
- {/* Status row */}
-
-
- {parsed.statusText}
-
- {getErrorTypeLabel(parsed.errorType)}
-
-
-
- {/* Key metrics grid */}
-
-
-
Method
-
{parsed.method || 'N/A'}
-
-
-
Provider
-
{parsed.provider || 'N/A'}
-
-
-
Version
-
{parsed.version || 'N/A'}
-
-
-
Endpoint
-
- {parsed.endpoint || 'N/A'}
-
-
-
-
- {/* URL */}
-
-
URL
-
- {parsed.url || 'N/A'}
-
-
-
- {/* Timestamp */}
-
-
Timestamp
-
{parsed.timestamp || 'N/A'}
-
-
- {/* Suggestion based on error type */}
- {parsed.errorType !== 'unknown' && (
-
-
-
- {parsed.errorType === 'rate_limit' &&
- 'Rate limited. Consider using multiple accounts or reducing request frequency.'}
- {parsed.errorType === 'auth' &&
- 'Authentication failed. Check credentials or re-authenticate with the provider.'}
- {parsed.errorType === 'not_found' &&
- 'Endpoint not found. This endpoint may not exist on this provider.'}
- {parsed.errorType === 'server' &&
- 'Server error from upstream. Retry or check provider status.'}
- {parsed.errorType === 'timeout' &&
- 'Request timed out. Check network or increase timeout settings.'}
-
-
- )}
-
- );
-}
-
-/** Headers tab content */
-function HeadersTab({ headers }: { headers: Record }) {
- const entries = Object.entries(headers);
- if (entries.length === 0) {
- return No headers available
;
- }
-
- return (
-
-
- {entries.map(([key, value]) => (
-
- {key}:
- {value}
-
- ))}
-
-
- );
-}
-
-/** JSON/Body tab content */
-function BodyTab({ content, label }: { content: string; label: string }) {
- if (!content || content.trim() === '') {
- return No {label.toLowerCase()} body
;
- }
-
- // Try to format as JSON
- let formatted = content;
- let isJson = false;
- try {
- const parsed = JSON.parse(content);
- formatted = JSON.stringify(parsed, null, 2);
- isJson = true;
- } catch {
- // Not JSON, use as-is
- }
-
- return (
-
-
- {formatted}
-
-
- );
-}
-
-/** Raw tab content */
-function RawTab({ content }: { content: string }) {
- return (
-
-
- {content}
-
-
- );
-}
-
-/** Log content panel with tabs */
-function LogContentPanel({ name, absolutePath }: { name: string | null; absolutePath?: string }) {
- const [activeTab, setActiveTab] = useState('overview');
- const { data: content, isLoading, error } = useCliproxyErrorLogContent(name);
-
- // Parse log content
- const parsed = useMemo(() => {
- if (!content) return null;
- return parseErrorLog(content);
- }, [content]);
-
- // No log selected
- if (!name) {
- return (
-
-
-
-
Select a log to view details
-
-
- );
- }
-
- // Loading state
- if (isLoading) {
- return (
-
-
-
-
-
-
- );
- }
-
- // Error or no content
- if (error || !content || !parsed) {
- return (
-
-
Failed to load log content
-
- );
- }
-
- return (
-
- {/* Header with status */}
-
-
-
-
- {parsed.provider}/{parsed.endpoint || 'unknown'}
-
- {/* Copy Absolute Path Button */}
- {name && (
-
- )}
-
-
- {/* Copy Raw Content Button */}
- {content && (
-
- )}
-
- {parsed.method}
-
-
-
-
- {/* Tabs */}
-
- setActiveTab('overview')}
- icon={Info}
- >
- Overview
-
- setActiveTab('headers')}
- icon={Code}
- >
- Headers
-
- setActiveTab('request')}
- icon={ArrowUpRight}
- >
- Request
-
- setActiveTab('response')}
- icon={ArrowDownLeft}
- >
- Response
-
- setActiveTab('raw')} icon={FileText}>
- Raw
-
-
-
- {/* Tab content */}
-
- {activeTab === 'overview' && }
- {activeTab === 'headers' && }
- {activeTab === 'request' && }
- {activeTab === 'response' && }
- {activeTab === 'raw' && }
-
-
- );
-}
-
-/** Error log item in the list */
-interface ErrorLogItemProps {
- name: string;
- size: number;
- modified: number;
- isSelected: boolean;
- onClick: () => void;
-}
-
-function ErrorLogItem({ name, size, modified, isSelected, onClick }: ErrorLogItemProps) {
- const parsed = useMemo(() => parseFilename(name), [name]);
-
- return (
-
- );
-}
-
-export function ErrorLogsMonitor() {
- const { data: status, isLoading: isStatusLoading } = useCliproxyStatus();
- const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running ?? false);
-
- // Vertical resize state
- const [height, setHeight] = useState(500);
- const [isResizing, setIsResizing] = useState(false);
- const containerRef = useRef(null);
- const scrollIntervalRef = useRef(null);
-
- // Auto-scroll handler
- const stopAutoScroll = () => {
- if (scrollIntervalRef.current) {
- clearInterval(scrollIntervalRef.current);
- scrollIntervalRef.current = null;
- }
- };
-
- // Resize handlers
- useEffect(() => {
- if (!isResizing) return;
-
- const handleMouseMove = (e: MouseEvent) => {
- const container = containerRef.current;
- if (!container) return;
-
- const rect = container.getBoundingClientRect();
- const containerTopDoc = rect.top + window.scrollY;
- const newHeight = e.pageY - containerTopDoc;
-
- // Constrain height (min 300, no max)
- setHeight(Math.max(300, newHeight));
-
- // Auto-scroll logic
- const viewportHeight = window.innerHeight;
- const distFromBottom = viewportHeight - e.clientY;
- const scrollSpeed = 15;
-
- stopAutoScroll();
-
- if (distFromBottom < 50) {
- scrollIntervalRef.current = setInterval(() => {
- window.scrollBy(0, scrollSpeed);
- }, 16);
- } else if (e.clientY < 50) {
- scrollIntervalRef.current = setInterval(() => {
- window.scrollBy(0, -scrollSpeed);
- }, 16);
- }
- };
-
- const handleMouseUp = () => {
- setIsResizing(false);
- stopAutoScroll();
- document.body.style.cursor = 'default';
- document.body.style.userSelect = 'auto';
- };
-
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = 'row-resize';
- document.body.style.userSelect = 'none';
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = 'default';
- document.body.style.userSelect = 'auto';
- stopAutoScroll();
- };
- }, [isResizing]);
-
- const startResizing = (e: React.MouseEvent) => {
- e.preventDefault();
- setIsResizing(true);
- };
-
- // Compute default selection (first log name or null)
- const defaultLogName = useMemo(() => logs?.[0]?.name ?? null, [logs]);
-
- // Use controlled selection that defaults to first log
- const [selectedLog, setSelectedLog] = useState(null);
-
- // Effective selection: use user selection if available, otherwise default
- const effectiveSelection = selectedLog ?? defaultLogName;
-
- // Get absolute path for the selected log
- const selectedAbsolutePath = useMemo(() => {
- if (!effectiveSelection || !logs) return undefined;
- const log = logs.find((l) => l.name === effectiveSelection);
- return log?.absolutePath;
- }, [effectiveSelection, logs]);
-
- // Guards
- if (isStatusLoading) return null;
- if (!status?.running) return null;
- if (isLoading) {
- return (
-
-
-
-
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
- );
- }
- if (!logs || logs.length === 0) return null;
-
- const errorCount = logs.length;
-
- return (
-
- {/* Header */}
-
-
-
-
Error Logs
-
- {errorCount} failed request{errorCount !== 1 ? 's' : ''}
-
-
-
-
- CLIProxy Diagnostics
-
-
-
- {/* Resizable Panel Layout */}
-
-
- {/* Left Panel: Log List */}
-
-
-
- {logs.slice(0, 50).map((log) => (
- setSelectedLog(log.name)}
- />
- ))}
-
- {logs.length > 50 && (
-
- Showing 50 of {logs.length} logs
-
- )}
-
-
-
- {/* Resize Handle */}
-
-
-
-
-
-
-
- {/* Right Panel: Log Content */}
-
-
-
-
-
-
- {/* Use standard footer if error, otherwise show resize handle */}
- {error ? (
-
- {error.message}
-
- ) : (
-
-
-
- )}
-
- );
-}
+export type { TabType, ErrorLogItemProps, LogContentPanelProps } from './monitoring/error-logs';
diff --git a/ui/src/components/monitoring/auth-monitor.tsx b/ui/src/components/monitoring/auth-monitor.tsx
new file mode 100644
index 00000000..61cbaf8b
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor.tsx
@@ -0,0 +1,465 @@
+/**
+ * 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/error-logs/error-log-item.tsx b/ui/src/components/monitoring/error-logs/error-log-item.tsx
new file mode 100644
index 00000000..a4d4b2e1
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/error-log-item.tsx
@@ -0,0 +1,73 @@
+/**
+ * Error Log Item Component
+ * Individual log entry in the list view
+ */
+
+import { useMemo } from 'react';
+import { cn } from '@/lib/utils';
+import { Clock, FileText } from 'lucide-react';
+import { ProviderIcon } from '@/components/shared/provider-icon';
+import { parseFilename, formatRelativeTime, formatBytes } from '@/lib/error-log-parser';
+import type { ErrorLogItemProps } from './types';
+
+export function ErrorLogItem({ name, size, modified, isSelected, onClick }: ErrorLogItemProps) {
+ const parsed = useMemo(() => parseFilename(name), [name]);
+
+ return (
+
+ );
+}
diff --git a/ui/src/components/monitoring/error-logs/index.tsx b/ui/src/components/monitoring/error-logs/index.tsx
new file mode 100644
index 00000000..b96a65c7
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/index.tsx
@@ -0,0 +1,219 @@
+/**
+ * Error Logs Monitor Component
+ *
+ * Displays CLIProxyAPI error logs with master-detail split view.
+ * ETL: Parses raw logs into structured data for rich display.
+ * - Left panel: Log list with status code, provider, endpoint, relative time
+ * - Right panel: Tabbed view (Overview, Headers, Request, Response, Raw)
+ */
+
+import { useState, useMemo, useRef, useEffect } from 'react';
+import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
+import { useCliproxyErrorLogs } from '@/hooks/use-cliproxy-stats';
+import { useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
+import { Skeleton } from '@/components/ui/skeleton';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { AlertTriangle, FileWarning, GripVertical, GripHorizontal } from 'lucide-react';
+import { ErrorLogItem } from './error-log-item';
+import { LogContentPanel } from './log-content-panel';
+
+export function ErrorLogsMonitor() {
+ const { data: status, isLoading: isStatusLoading } = useCliproxyStatus();
+ const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running ?? false);
+
+ // Vertical resize state
+ const [height, setHeight] = useState(500);
+ const [isResizing, setIsResizing] = useState(false);
+ const containerRef = useRef(null);
+ const scrollIntervalRef = useRef(null);
+
+ // Auto-scroll handler
+ const stopAutoScroll = () => {
+ if (scrollIntervalRef.current) {
+ clearInterval(scrollIntervalRef.current);
+ scrollIntervalRef.current = null;
+ }
+ };
+
+ // Resize handlers
+ useEffect(() => {
+ if (!isResizing) return;
+
+ const handleMouseMove = (e: MouseEvent) => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const rect = container.getBoundingClientRect();
+ const containerTopDoc = rect.top + window.scrollY;
+ const newHeight = e.pageY - containerTopDoc;
+
+ // Constrain height (min 300, no max)
+ setHeight(Math.max(300, newHeight));
+
+ // Auto-scroll logic
+ const viewportHeight = window.innerHeight;
+ const distFromBottom = viewportHeight - e.clientY;
+ const scrollSpeed = 15;
+
+ stopAutoScroll();
+
+ if (distFromBottom < 50) {
+ scrollIntervalRef.current = setInterval(() => {
+ window.scrollBy(0, scrollSpeed);
+ }, 16);
+ } else if (e.clientY < 50) {
+ scrollIntervalRef.current = setInterval(() => {
+ window.scrollBy(0, -scrollSpeed);
+ }, 16);
+ }
+ };
+
+ const handleMouseUp = () => {
+ setIsResizing(false);
+ stopAutoScroll();
+ document.body.style.cursor = 'default';
+ document.body.style.userSelect = 'auto';
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'row-resize';
+ document.body.style.userSelect = 'none';
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'default';
+ document.body.style.userSelect = 'auto';
+ stopAutoScroll();
+ };
+ }, [isResizing]);
+
+ const startResizing = (e: React.MouseEvent) => {
+ e.preventDefault();
+ setIsResizing(true);
+ };
+
+ // Compute default selection (first log name or null)
+ const defaultLogName = useMemo(() => logs?.[0]?.name ?? null, [logs]);
+
+ // Use controlled selection that defaults to first log
+ const [selectedLog, setSelectedLog] = useState(null);
+
+ // Effective selection: use user selection if available, otherwise default
+ const effectiveSelection = selectedLog ?? defaultLogName;
+
+ // Get absolute path for the selected log
+ const selectedAbsolutePath = useMemo(() => {
+ if (!effectiveSelection || !logs) return undefined;
+ const log = logs.find((l) => l.name === effectiveSelection);
+ return log?.absolutePath;
+ }, [effectiveSelection, logs]);
+
+ // Guards
+ if (isStatusLoading) return null;
+ if (!status?.running) return null;
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ );
+ }
+ if (!logs || logs.length === 0) return null;
+
+ const errorCount = logs.length;
+
+ return (
+
+ {/* Header */}
+
+
+
+
Error Logs
+
+ {errorCount} failed request{errorCount !== 1 ? 's' : ''}
+
+
+
+
+ CLIProxy Diagnostics
+
+
+
+ {/* Resizable Panel Layout */}
+
+
+ {/* Left Panel: Log List */}
+
+
+
+ {logs.slice(0, 50).map((log) => (
+ setSelectedLog(log.name)}
+ />
+ ))}
+
+ {logs.length > 50 && (
+
+ Showing 50 of {logs.length} logs
+
+ )}
+
+
+
+ {/* Resize Handle */}
+
+
+
+
+
+
+
+ {/* Right Panel: Log Content */}
+
+
+
+
+
+
+ {/* Use standard footer if error, otherwise show resize handle */}
+ {error ? (
+
+ {error.message}
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
+
+// Re-export components
+export { ErrorLogItem } from './error-log-item';
+export { LogContentPanel } from './log-content-panel';
+export { TabButton, StatusBadge } from './ui-primitives';
+export { OverviewTab, HeadersTab, BodyTab, RawTab } from './tab-components';
+export type { TabType, ErrorLogItemProps, LogContentPanelProps } from './types';
diff --git a/ui/src/components/monitoring/error-logs/log-content-panel.tsx b/ui/src/components/monitoring/error-logs/log-content-panel.tsx
new file mode 100644
index 00000000..45804590
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/log-content-panel.tsx
@@ -0,0 +1,140 @@
+/**
+ * Log Content Panel Component
+ * Detail view with tabbed navigation for error log content
+ */
+
+import { useState, useMemo } from 'react';
+import { useCliproxyErrorLogContent } from '@/hooks/use-cliproxy-stats';
+import { Skeleton } from '@/components/ui/skeleton';
+import { CopyButton } from '@/components/ui/copy-button';
+import { Terminal, Info, Code, ArrowUpRight, ArrowDownLeft, FileText } from 'lucide-react';
+import { parseErrorLog } from '@/lib/error-log-parser';
+import type { TabType, LogContentPanelProps } from './types';
+import { TabButton, StatusBadge } from './ui-primitives';
+import { OverviewTab, HeadersTab, BodyTab, RawTab } from './tab-components';
+
+export function LogContentPanel({ name, absolutePath }: LogContentPanelProps) {
+ const [activeTab, setActiveTab] = useState('overview');
+ const { data: content, isLoading, error } = useCliproxyErrorLogContent(name);
+
+ // Parse log content
+ const parsed = useMemo(() => {
+ if (!content) return null;
+ return parseErrorLog(content);
+ }, [content]);
+
+ // No log selected
+ if (!name) {
+ return (
+
+
+
+
Select a log to view details
+
+
+ );
+ }
+
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ // Error or no content
+ if (error || !content || !parsed) {
+ return (
+
+
Failed to load log content
+
+ );
+ }
+
+ return (
+
+ {/* Header with status */}
+
+
+
+
+ {parsed.provider}/{parsed.endpoint || 'unknown'}
+
+ {/* Copy Absolute Path Button */}
+ {name && (
+
+ )}
+
+
+ {/* Copy Raw Content Button */}
+ {content && (
+
+ )}
+
+ {parsed.method}
+
+
+
+
+ {/* Tabs */}
+
+ setActiveTab('overview')}
+ icon={Info}
+ >
+ Overview
+
+ setActiveTab('headers')}
+ icon={Code}
+ >
+ Headers
+
+ setActiveTab('request')}
+ icon={ArrowUpRight}
+ >
+ Request
+
+ setActiveTab('response')}
+ icon={ArrowDownLeft}
+ >
+ Response
+
+ setActiveTab('raw')} icon={FileText}>
+ Raw
+
+
+
+ {/* Tab content */}
+
+ {activeTab === 'overview' && }
+ {activeTab === 'headers' && }
+ {activeTab === 'request' && }
+ {activeTab === 'response' && }
+ {activeTab === 'raw' && }
+
+
+ );
+}
diff --git a/ui/src/components/monitoring/error-logs/tab-components.tsx b/ui/src/components/monitoring/error-logs/tab-components.tsx
new file mode 100644
index 00000000..42043731
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/tab-components.tsx
@@ -0,0 +1,149 @@
+/**
+ * Tab Content Components for Error Logs Monitor
+ * OverviewTab, HeadersTab, BodyTab, RawTab
+ */
+
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Info } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { getErrorTypeLabel, type ParsedErrorLog } from '@/lib/error-log-parser';
+import { StatusBadge } from './ui-primitives';
+
+/** Overview tab content */
+export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
+ return (
+
+ {/* Status row */}
+
+
+ {parsed.statusText}
+
+ {getErrorTypeLabel(parsed.errorType)}
+
+
+
+ {/* Key metrics grid */}
+
+
+
Method
+
{parsed.method || 'N/A'}
+
+
+
Provider
+
{parsed.provider || 'N/A'}
+
+
+
Version
+
{parsed.version || 'N/A'}
+
+
+
Endpoint
+
+ {parsed.endpoint || 'N/A'}
+
+
+
+
+ {/* URL */}
+
+
URL
+
+ {parsed.url || 'N/A'}
+
+
+
+ {/* Timestamp */}
+
+
Timestamp
+
{parsed.timestamp || 'N/A'}
+
+
+ {/* Suggestion based on error type */}
+ {parsed.errorType !== 'unknown' && (
+
+
+
+ {parsed.errorType === 'rate_limit' &&
+ 'Rate limited. Consider using multiple accounts or reducing request frequency.'}
+ {parsed.errorType === 'auth' &&
+ 'Authentication failed. Check credentials or re-authenticate with the provider.'}
+ {parsed.errorType === 'not_found' &&
+ 'Endpoint not found. This endpoint may not exist on this provider.'}
+ {parsed.errorType === 'server' &&
+ 'Server error from upstream. Retry or check provider status.'}
+ {parsed.errorType === 'timeout' &&
+ 'Request timed out. Check network or increase timeout settings.'}
+
+
+ )}
+
+ );
+}
+
+/** Headers tab content */
+export function HeadersTab({ headers }: { headers: Record }) {
+ const entries = Object.entries(headers);
+ if (entries.length === 0) {
+ return No headers available
;
+ }
+
+ return (
+
+
+ {entries.map(([key, value]) => (
+
+ {key}:
+ {value}
+
+ ))}
+
+
+ );
+}
+
+/** JSON/Body tab content */
+export function BodyTab({ content, label }: { content: string; label: string }) {
+ if (!content || content.trim() === '') {
+ return No {label.toLowerCase()} body
;
+ }
+
+ // Try to format as JSON
+ let formatted = content;
+ let isJson = false;
+ try {
+ const parsed = JSON.parse(content);
+ formatted = JSON.stringify(parsed, null, 2);
+ isJson = true;
+ } catch {
+ // Not JSON, use as-is
+ }
+
+ return (
+
+
+ {formatted}
+
+
+ );
+}
+
+/** Raw tab content */
+export function RawTab({ content }: { content: string }) {
+ return (
+
+
+ {content}
+
+
+ );
+}
diff --git a/ui/src/components/monitoring/error-logs/types.ts b/ui/src/components/monitoring/error-logs/types.ts
new file mode 100644
index 00000000..0cae04f1
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/types.ts
@@ -0,0 +1,18 @@
+/**
+ * Types for Error Logs Monitor
+ */
+
+export type TabType = 'overview' | 'headers' | 'request' | 'response' | 'raw';
+
+export interface ErrorLogItemProps {
+ name: string;
+ size: number;
+ modified: number;
+ isSelected: boolean;
+ onClick: () => void;
+}
+
+export interface LogContentPanelProps {
+ name: string | null;
+ absolutePath?: string;
+}
diff --git a/ui/src/components/monitoring/error-logs/ui-primitives.tsx b/ui/src/components/monitoring/error-logs/ui-primitives.tsx
new file mode 100644
index 00000000..535ad183
--- /dev/null
+++ b/ui/src/components/monitoring/error-logs/ui-primitives.tsx
@@ -0,0 +1,51 @@
+/**
+ * UI Primitives for Error Logs Monitor
+ * TabButton and StatusBadge components
+ */
+
+import { cn } from '@/lib/utils';
+import { getStatusColor } from '@/lib/error-log-parser';
+
+/** Tab button component */
+export function TabButton({
+ active,
+ onClick,
+ children,
+ icon: Icon,
+}: {
+ active: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+ icon?: React.ComponentType<{ className?: string }>;
+}) {
+ return (
+
+ );
+}
+
+/** Status badge component */
+export function StatusBadge({ code }: { code: number }) {
+ const colorClass = getStatusColor(code);
+ return (
+
+ {code}
+
+ );
+}
diff --git a/ui/src/components/monitoring/index.ts b/ui/src/components/monitoring/index.ts
new file mode 100644
index 00000000..3d66966d
--- /dev/null
+++ b/ui/src/components/monitoring/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Monitoring Components Barrel Export
+ */
+
+// Main monitoring components
+export { AuthMonitor } from './auth-monitor';
+export { ProxyStatusWidget } from './proxy-status-widget';
+
+// Error logs (from subdirectory)
+export { ErrorLogsMonitor } from './error-logs';
+export type { TabType, ErrorLogItemProps, LogContentPanelProps } from './error-logs';
diff --git a/ui/src/components/monitoring/proxy-status-widget.tsx b/ui/src/components/monitoring/proxy-status-widget.tsx
new file mode 100644
index 00000000..bfa0a2c3
--- /dev/null
+++ b/ui/src/components/monitoring/proxy-status-widget.tsx
@@ -0,0 +1,196 @@
+/**
+ * Proxy Status Widget
+ *
+ * Displays CLIProxy process status with start/stop/restart controls.
+ * Shows: running state, port, session count, uptime, update availability.
+ */
+
+import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw, ArrowUp } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import {
+ useProxyStatus,
+ useStartProxy,
+ useStopProxy,
+ useCliproxyUpdateCheck,
+} from '@/hooks/use-cliproxy';
+import { cn } from '@/lib/utils';
+
+function formatUptime(startedAt?: string): string {
+ if (!startedAt) return '';
+ const start = new Date(startedAt).getTime();
+ const now = Date.now();
+ const diff = now - start;
+
+ const hours = Math.floor(diff / (1000 * 60 * 60));
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ }
+ return `${minutes}m`;
+}
+
+function formatTimeAgo(timestamp?: number): string {
+ if (!timestamp) return '';
+ const diff = Date.now() - timestamp;
+ const minutes = Math.floor(diff / (1000 * 60));
+ const hours = Math.floor(diff / (1000 * 60 * 60));
+
+ if (minutes < 1) return 'just now';
+ if (minutes < 60) return `${minutes}m ago`;
+ return `${hours}h ago`;
+}
+
+export function ProxyStatusWidget() {
+ const { data: status, isLoading } = useProxyStatus();
+ const { data: updateCheck } = useCliproxyUpdateCheck();
+ const startProxy = useStartProxy();
+ const stopProxy = useStopProxy();
+
+ const isRunning = status?.running ?? false;
+ const isActioning = startProxy.isPending || stopProxy.isPending;
+ const hasUpdate = updateCheck?.hasUpdate ?? false;
+
+ // Restart = stop then start
+ const handleRestart = async () => {
+ await stopProxy.mutateAsync();
+ // Small delay to ensure port is released
+ await new Promise((r) => setTimeout(r, 500));
+ startProxy.mutate();
+ };
+
+ return (
+
+
+
+
+
CLIProxy Service
+ {hasUpdate && (
+
v${updateCheck?.latestVersion}`}
+ >
+
+ Update
+
+ )}
+
+
+
+ {isLoading ? (
+
+ ) : isRunning ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isRunning && status ? (
+ <>
+
+ Port {status.port}
+ {status.sessionCount !== undefined && status.sessionCount > 0 && (
+
+
+ {status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''}
+
+ )}
+ {status.startedAt && (
+
+
+ {formatUptime(status.startedAt)}
+
+ )}
+
+ {/* Control buttons when running */}
+
+
+
+
+ >
+ ) : (
+
+
Not running
+
+
+ )}
+
+ {/* Version sync indicator */}
+ {updateCheck?.currentVersion && (
+
+ v{updateCheck.currentVersion}
+ {updateCheck.checkedAt && (
+
+ Synced {formatTimeAgo(updateCheck.checkedAt)}
+
+ )}
+
+ )}
+
+ );
+}