From 5b3d56548a8dfb2e6bb22e14b13f0fb038f2d1fb Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 02:15:35 -0500 Subject: [PATCH 1/4] feat(dashboard): add error log viewer for CLIProxy diagnostics Add ErrorLogsMonitor component to Home page that displays CLIProxyAPI error logs when requests fail. Users can now diagnose why success rates drop by viewing detailed error log contents. Backend: - Add fetchCliproxyErrorLogs/fetchCliproxyErrorLogContent in stats-fetcher - Add GET /api/cliproxy/error-logs and /api/cliproxy/error-logs/:name routes - Include path traversal protection for filename validation Frontend: - Add CliproxyErrorLog type and errorLogs API methods - Add useCliproxyErrorLogs/useCliproxyErrorLogContent hooks - Create ErrorLogsMonitor component with expandable log viewer - Integrate into Home page below AuthMonitor Closes #132 --- src/cliproxy/stats-fetcher.ts | 84 +++++++++ src/web-server/routes.ts | 73 ++++++++ ui/src/components/error-logs-monitor.tsx | 212 +++++++++++++++++++++++ ui/src/hooks/use-cliproxy-stats.ts | 57 ++++++ ui/src/lib/api-client.ts | 21 +++ ui/src/pages/home.tsx | 4 + 6 files changed, 451 insertions(+) create mode 100644 ui/src/components/error-logs-monitor.tsx diff --git a/src/cliproxy/stats-fetcher.ts b/src/cliproxy/stats-fetcher.ts index aaf00058..cafbc526 100644 --- a/src/cliproxy/stats-fetcher.ts +++ b/src/cliproxy/stats-fetcher.ts @@ -271,6 +271,90 @@ export async function fetchCliproxyModels( } } +/** Error log file metadata from CLIProxyAPI */ +export interface CliproxyErrorLog { + /** Filename (e.g., "error-v1-chat-completions-2025-01-15T10-30-00.log") */ + name: string; + /** File size in bytes */ + size: number; + /** Last modified timestamp (Unix seconds) */ + modified: number; +} + +/** Response from /v0/management/request-error-logs endpoint */ +interface ErrorLogsApiResponse { + files: CliproxyErrorLog[]; +} + +/** + * Fetch error log file list from CLIProxyAPI management API + * @param port CLIProxyAPI port (default: 8317) + * @returns Array of error log metadata or null if unavailable + */ +export async function fetchCliproxyErrorLogs( + port: number = CLIPROXY_DEFAULT_PORT +): Promise { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + + const response = await fetch(`http://127.0.0.1:${port}/v0/management/request-error-logs`, { + signal: controller.signal, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${CCS_CONTROL_PANEL_SECRET}`, + }, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return null; + } + + const data = (await response.json()) as ErrorLogsApiResponse; + return data.files ?? []; + } catch { + return null; + } +} + +/** + * Fetch error log file content from CLIProxyAPI management API + * @param name Error log filename + * @param port CLIProxyAPI port (default: 8317) + * @returns Log file content as string or null if unavailable + */ +export async function fetchCliproxyErrorLogContent( + name: string, + port: number = CLIPROXY_DEFAULT_PORT +): Promise { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch( + `http://127.0.0.1:${port}/v0/management/request-error-logs/${encodeURIComponent(name)}`, + { + signal: controller.signal, + headers: { + Authorization: `Bearer ${CCS_CONTROL_PANEL_SECRET}`, + }, + } + ); + + clearTimeout(timeoutId); + + if (!response.ok) { + return null; + } + + return await response.text(); + } catch { + return null; + } +} + /** * Check if CLIProxyAPI is running and responsive * @param port CLIProxyAPI port (default: 8317) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 9a4486fd..7e9a44a5 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -21,6 +21,8 @@ import { fetchCliproxyStats, fetchCliproxyModels, isCliproxyRunning, + fetchCliproxyErrorLogs, + fetchCliproxyErrorLogContent, } from '../cliproxy/stats-fetcher'; import { listOpenAICompatProviders, @@ -1374,6 +1376,77 @@ apiRoutes.get('/cliproxy/models', async (_req: Request, res: Response): Promise< } }); +// ==================== Error Logs ==================== + +/** + * GET /api/cliproxy/error-logs - Get list of error log files + * Returns: { files: CliproxyErrorLog[] } or error if proxy not running + */ +apiRoutes.get('/cliproxy/error-logs', async (_req: Request, res: Response): Promise => { + try { + const running = await isCliproxyRunning(); + if (!running) { + res.status(503).json({ + error: 'CLIProxyAPI not running', + message: 'Start a CLIProxy session to view error logs', + }); + return; + } + + const files = await fetchCliproxyErrorLogs(); + if (files === null) { + res.status(503).json({ + error: 'Error logs unavailable', + message: 'CLIProxyAPI is running but error logs endpoint not responding', + }); + return; + } + + res.json({ files }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + +/** + * GET /api/cliproxy/error-logs/:name - Get content of a specific error log + * Returns: plain text log content + */ +apiRoutes.get('/cliproxy/error-logs/:name', async (req: Request, res: Response): Promise => { + const { name } = req.params; + + // Validate filename format and prevent path traversal + if ( + !name || + !name.startsWith('error-') || + !name.endsWith('.log') || + name.includes('..') || + name.includes('/') || + name.includes('\\') + ) { + res.status(400).json({ error: 'Invalid error log filename' }); + return; + } + + try { + const running = await isCliproxyRunning(); + if (!running) { + res.status(503).json({ error: 'CLIProxyAPI not running' }); + return; + } + + const content = await fetchCliproxyErrorLogContent(name); + if (content === null) { + res.status(404).json({ error: 'Error log not found' }); + return; + } + + res.type('text/plain').send(content); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + // ============================================ // OpenAI Compatibility Layer Routes // ============================================ diff --git a/ui/src/components/error-logs-monitor.tsx b/ui/src/components/error-logs-monitor.tsx new file mode 100644 index 00000000..4aeaf45a --- /dev/null +++ b/ui/src/components/error-logs-monitor.tsx @@ -0,0 +1,212 @@ +/** + * Error Logs Monitor Component + * + * Displays CLIProxyAPI error logs with expandable details. + * Designed to complement the AuthMonitor on the Home page. + */ + +import { useState } from 'react'; +import { useCliproxyErrorLogs, useCliproxyErrorLogContent } from '@/hooks/use-cliproxy-stats'; +import { useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; +import { cn, STATUS_COLORS } from '@/lib/utils'; +import { Skeleton } from '@/components/ui/skeleton'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + AlertTriangle, + ChevronDown, + ChevronRight, + FileWarning, + Clock, + FileText, + XCircle, +} from 'lucide-react'; + +/** Format file size in human-readable format */ +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +/** Format timestamp to relative time */ +function formatRelativeTime(unixSeconds: number): string { + const diff = Math.floor(Date.now() / 1000 - unixSeconds); + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; +} + +/** Parse error log filename to extract endpoint and timestamp */ +function parseErrorLogName(name: string): { endpoint: string; timestamp: string } { + // Format: error-v1-chat-completions-2025-01-15T10-30-00.log + const match = name.match(/^error-(.+)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/); + if (match) { + const endpoint = match[1].replace(/-/g, '/'); + const timestamp = match[2].replace(/T/, ' ').replace(/-/g, ':'); + return { endpoint: `/${endpoint}`, timestamp }; + } + return { endpoint: name, timestamp: '' }; +} + +/** Error log content viewer with syntax highlighting */ +function ErrorLogContent({ name }: { name: string }) { + const { data: content, isLoading, error } = useCliproxyErrorLogContent(name); + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (error || !content) { + return ( +
Failed to load error log content
+ ); + } + + return ( + +
+        {content}
+      
+
+ ); +} + +export function ErrorLogsMonitor() { + const { data: status } = useCliproxyStatus(); + const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running); + const [expandedLog, setExpandedLog] = useState(null); + + // Don't show if proxy not running or loading + if (!status?.running) { + return null; + } + + if (isLoading) { + return ( +
+
+ + +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ); + } + + // Don't show if no errors (good state) + if (!logs || logs.length === 0) { + return null; + } + + const errorCount = logs.length; + + return ( +
+ {/* Header with warning styling */} +
+
+
+ +
+ Error Logs + + {errorCount} failed request{errorCount !== 1 ? 's' : ''} + +
+
+ + CLIProxy Diagnostics +
+
+ + {/* Error logs list */} + +
+ {logs.slice(0, 10).map((log) => { + const isExpanded = expandedLog === log.name; + const { endpoint, timestamp } = parseErrorLogName(log.name); + + return ( +
+ + + {/* Expandable content */} + {isExpanded && ( +
+ +
+ )} +
+ ); + })} +
+ + {/* Show more indicator */} + {logs.length > 10 && ( +
+ Showing 10 of {logs.length} error logs +
+ )} +
+ + {/* Footer hint */} + {error && ( +
+ {error.message} +
+ )} +
+ ); +} diff --git a/ui/src/hooks/use-cliproxy-stats.ts b/ui/src/hooks/use-cliproxy-stats.ts index 3d27c7a9..3bea8b29 100644 --- a/ui/src/hooks/use-cliproxy-stats.ts +++ b/ui/src/hooks/use-cliproxy-stats.ts @@ -130,3 +130,60 @@ export function useCliproxyModels(enabled = true) { retry: 1, }); } + +/** Error log file metadata from CLIProxyAPI */ +export interface CliproxyErrorLog { + name: string; + size: number; + modified: number; +} + +/** + * Fetch CLIProxy error logs from API + */ +async function fetchCliproxyErrorLogs(): Promise { + const response = await fetch('/api/cliproxy/error-logs'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to fetch error logs'); + } + const data = await response.json(); + return data.files ?? []; +} + +/** + * Fetch specific error log content + */ +async function fetchCliproxyErrorLogContent(name: string): Promise { + const response = await fetch(`/api/cliproxy/error-logs/${encodeURIComponent(name)}`); + if (!response.ok) { + throw new Error('Failed to fetch error log content'); + } + return response.text(); +} + +/** + * Hook to get CLIProxy error logs list + */ +export function useCliproxyErrorLogs(enabled = true) { + return useQuery({ + queryKey: ['cliproxy-error-logs'], + queryFn: fetchCliproxyErrorLogs, + enabled, + refetchInterval: 30000, // Refresh every 30 seconds + retry: 1, + staleTime: 10000, + }); +} + +/** + * Hook to get specific error log content + */ +export function useCliproxyErrorLogContent(name: string | null) { + return useQuery({ + queryKey: ['cliproxy-error-log-content', name], + queryFn: () => (name ? fetchCliproxyErrorLogContent(name) : Promise.resolve('')), + enabled: !!name, + staleTime: 60000, // Cache log content for 1 minute + }); +} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 7ca955b4..9732272b 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -163,6 +163,16 @@ export interface ProxyProcessStatus { startedAt?: string; } +/** Error log file metadata from CLIProxyAPI */ +export interface CliproxyErrorLog { + /** Filename (e.g., "error-v1-chat-completions-2025-01-15T10-30-00.log") */ + name: string; + /** File size in bytes */ + size: number; + /** Last modified timestamp (Unix seconds) */ + modified: number; +} + /** Result from starting proxy service */ export interface ProxyStartResult { started: boolean; @@ -266,6 +276,17 @@ export const api = { body: JSON.stringify({ nickname }), }), }, + // Error logs + errorLogs: { + /** List error log files */ + list: () => request<{ files: CliproxyErrorLog[] }>('/cliproxy/error-logs'), + /** Get content of a specific error log */ + getContent: async (name: string): Promise => { + const res = await fetch(`${BASE_URL}/cliproxy/error-logs/${encodeURIComponent(name)}`); + if (!res.ok) throw new Error('Failed to load error log'); + return res.text(); + }, + }, }, accounts: { list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'), diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index ccc5bf92..e864b87a 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -1,6 +1,7 @@ import { useNavigate } from 'react-router-dom'; import { HeroSection } from '@/components/hero-section'; import { AuthMonitor } from '@/components/auth-monitor'; +import { ErrorLogsMonitor } from '@/components/error-logs-monitor'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Skeleton } from '@/components/ui/skeleton'; import { Key, Zap, Users, Activity, AlertTriangle } from 'lucide-react'; @@ -175,6 +176,9 @@ export function HomePage() { {/* Auth Monitor */} + + {/* Error Logs Monitor - shows only when there are errors */} + ); } From 1ef625ee863c517a5fbba21f16cf991bb77be7d7 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 02:45:58 -0500 Subject: [PATCH 2/4] fix(error-logs-monitor): properly handle status loading state - Add isStatusLoading check to prevent false-negative rendering - Use nullish coalescing for enabled param to ensure boolean value - Component now waits for status before deciding visibility Fixes initial render returning null due to undefined status --- ui/src/components/error-logs-monitor.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/src/components/error-logs-monitor.tsx b/ui/src/components/error-logs-monitor.tsx index 4aeaf45a..74418e3a 100644 --- a/ui/src/components/error-logs-monitor.tsx +++ b/ui/src/components/error-logs-monitor.tsx @@ -79,11 +79,15 @@ function ErrorLogContent({ name }: { name: string }) { } export function ErrorLogsMonitor() { - const { data: status } = useCliproxyStatus(); - const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running); + const { data: status, isLoading: isStatusLoading } = useCliproxyStatus(); + const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running ?? false); const [expandedLog, setExpandedLog] = useState(null); - // Don't show if proxy not running or loading + // Don't show while status is loading or if proxy not running + if (isStatusLoading) { + return null; + } + if (!status?.running) { return null; } From 6b9396fbc6d464bc3e3d6d3bb639e70fe5306074 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 02:54:46 -0500 Subject: [PATCH 3/4] feat(cliproxy): set WRITABLE_PATH for log storage in ~/.ccs/cliproxy/ - Add getCliproxyWritablePath() helper function - Set WRITABLE_PATH env var when spawning CLIProxy in both executors - Logs will now be stored in ~/.ccs/cliproxy/logs/ instead of CWD - Enables error log viewer to find logs in predictable location Note: CLIProxyAPI still has hardcoded MaxBackups=0 (unlimited). Log rotation should be addressed in CLIProxyAPI upstream. --- src/cliproxy/cliproxy-executor.ts | 5 +++++ src/cliproxy/config-generator.ts | 9 +++++++++ src/cliproxy/service-manager.ts | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index bef6a2fa..1bad1d16 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -24,6 +24,7 @@ import { getProviderConfig, ensureProviderSettings, CLIPROXY_DEFAULT_PORT, + getCliproxyWritablePath, } from './config-generator'; import { isAuthenticated } from './auth-handler'; import { CLIProxyProvider, ExecutorConfig } from './types'; @@ -369,6 +370,10 @@ export async function execClaudeWithCLIProxy( proxy = spawn(binaryPath, proxyArgs, { stdio: ['ignore', 'ignore', 'ignore'], detached: true, // Persist after parent terminal closes + env: { + ...process.env, + WRITABLE_PATH: getCliproxyWritablePath(), // Logs stored in ~/.ccs/cliproxy/logs/ + }, }); // Unref so parent process can exit independently diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 3383d313..7f572dba 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -30,6 +30,15 @@ export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed'; /** Simple secret key for Control Panel login (user-facing) */ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; +/** + * Get CLIProxy writable directory for logs and runtime files. + * This directory is set as WRITABLE_PATH env var when spawning CLIProxy. + * Logs will be stored in ~/.ccs/cliproxy/logs/ + */ +export function getCliproxyWritablePath(): string { + return path.join(getCcsDir(), 'cliproxy'); +} + /** * Config version - bump when config format changes to trigger regeneration * v1: Initial config (port, auth-dir, api-keys only) diff --git a/src/cliproxy/service-manager.ts b/src/cliproxy/service-manager.ts index a515dcc0..f6977a07 100644 --- a/src/cliproxy/service-manager.ts +++ b/src/cliproxy/service-manager.ts @@ -19,6 +19,7 @@ import { regenerateConfig, configNeedsRegeneration, CLIPROXY_DEFAULT_PORT, + getCliproxyWritablePath, } from './config-generator'; import { isCliproxyRunning } from './stats-fetcher'; @@ -171,6 +172,10 @@ export async function ensureCliproxyService( proxyProcess = spawn(binaryPath, proxyArgs, { stdio: ['ignore', verbose ? 'pipe' : 'ignore', verbose ? 'pipe' : 'ignore'], detached: true, // Allow process to run independently + env: { + ...process.env, + WRITABLE_PATH: getCliproxyWritablePath(), // Logs stored in ~/.ccs/cliproxy/logs/ + }, }); // Forward output in verbose mode From 8f47b8775f2c2493c05ee2be861ca3f8667cfc0e Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 18:23:38 -0500 Subject: [PATCH 4/4] feat(ui): redesign error logs monitor with split view layout - Replace dropdown accordion with master-detail split view - Add log list panel (240px) on left with selection state - Add content viewer panel on right for better readability - Add demo mode prop for UI testing - Auto-select first log via useMemo/derived state --- ui/src/components/error-logs-monitor.tsx | 361 +++++++++++++++-------- 1 file changed, 243 insertions(+), 118 deletions(-) diff --git a/ui/src/components/error-logs-monitor.tsx b/ui/src/components/error-logs-monitor.tsx index 74418e3a..b399a67b 100644 --- a/ui/src/components/error-logs-monitor.tsx +++ b/ui/src/components/error-logs-monitor.tsx @@ -1,11 +1,11 @@ /** * Error Logs Monitor Component * - * Displays CLIProxyAPI error logs with expandable details. - * Designed to complement the AuthMonitor on the Home page. + * Displays CLIProxyAPI error logs with master-detail split view. + * Log list on left, content panel on right for better readability. */ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useCliproxyErrorLogs, useCliproxyErrorLogContent } from '@/hooks/use-cliproxy-stats'; import { useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { cn, STATUS_COLORS } from '@/lib/utils'; @@ -13,14 +13,85 @@ import { Skeleton } from '@/components/ui/skeleton'; import { ScrollArea } from '@/components/ui/scroll-area'; import { AlertTriangle, - ChevronDown, - ChevronRight, FileWarning, Clock, FileText, XCircle, + FlaskConical, + Terminal, } from 'lucide-react'; +/** Demo mode mock data */ +const DEMO_ERROR_LOGS = [ + { + name: 'error-v1-chat-completions-2025-12-18T17-45-23.log', + size: 4523, + modified: Math.floor(Date.now() / 1000) - 120, + }, + { + name: 'error-v1-messages-2025-12-18T17-30-15.log', + size: 8912, + modified: Math.floor(Date.now() / 1000) - 900, + }, + { + name: 'error-v1-chat-completions-2025-12-18T16-22-08.log', + size: 2341, + modified: Math.floor(Date.now() / 1000) - 5400, + }, + { + name: 'error-v1-models-2025-12-18T14-10-55.log', + size: 1024, + modified: Math.floor(Date.now() / 1000) - 12600, + }, +]; + +const DEMO_LOG_CONTENT = `================================================================================ +REQUEST ERROR LOG +================================================================================ +Timestamp: 2025-12-18T17:45:23Z +Endpoint: /v1/chat/completions +Method: POST +Status: 429 Too Many Requests + +-------------------------------------------------------------------------------- +REQUEST HEADERS +-------------------------------------------------------------------------------- +Content-Type: application/json +Authorization: Bearer ccs-internal-managed +User-Agent: claude-code/1.0 + +-------------------------------------------------------------------------------- +REQUEST BODY +-------------------------------------------------------------------------------- +{ + "model": "gemini-claude-opus-4-5-thinking", + "messages": [ + {"role": "user", "content": "Explain quantum computing"} + ], + "max_tokens": 4096, + "stream": true +} + +-------------------------------------------------------------------------------- +RESPONSE +-------------------------------------------------------------------------------- +{ + "error": { + "message": "Rate limit exceeded. Please retry after 60 seconds.", + "type": "rate_limit_error", + "code": "rate_limit_exceeded" + } +} + +-------------------------------------------------------------------------------- +UPSTREAM API ERROR +-------------------------------------------------------------------------------- +Provider: gemini +Account: user@gmail.com +Quota: Daily limit reached (1500/1500 requests) +Retry-After: 60 +================================================================================`; + /** Format file size in human-readable format */ function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -39,7 +110,6 @@ function formatRelativeTime(unixSeconds: number): string { /** Parse error log filename to extract endpoint and timestamp */ function parseErrorLogName(name: string): { endpoint: string; timestamp: string } { - // Format: error-v1-chat-completions-2025-01-15T10-30-00.log const match = name.match(/^error-(.+)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/); if (match) { const endpoint = match[1].replace(/-/g, '/'); @@ -49,84 +119,181 @@ function parseErrorLogName(name: string): { endpoint: string; timestamp: string return { endpoint: name, timestamp: '' }; } -/** Error log content viewer with syntax highlighting */ -function ErrorLogContent({ name }: { name: string }) { - const { data: content, isLoading, error } = useCliproxyErrorLogContent(name); +/** Log content panel component */ +function LogContentPanel({ name, demo = false }: { name: string | null; demo?: boolean }) { + const { data: content, isLoading, error } = useCliproxyErrorLogContent(demo ? null : name); + // No log selected + if (!name) { + return ( +
+
+ +

Select a log to view details

+
+
+ ); + } + + // Demo mode + if (demo) { + const { endpoint } = parseErrorLogName(name); + return ( +
+
+ + {endpoint} +
+ +
+            {DEMO_LOG_CONTENT}
+          
+
+
+ ); + } + + // Loading state if (isLoading) { return ( -
+
+
); } + // Error or no content if (error || !content) { return ( -
Failed to load error log content
+
+

Failed to load log content

+
); } + // Show content + const { endpoint } = parseErrorLogName(name); return ( - -
-        {content}
-      
-
+
+
+ + {endpoint} +
+ +
+          {content}
+        
+
+
); } -export function ErrorLogsMonitor() { - const { data: status, isLoading: isStatusLoading } = useCliproxyStatus(); - const { data: logs, isLoading, error } = useCliproxyErrorLogs(status?.running ?? false); - const [expandedLog, setExpandedLog] = useState(null); +/** Error log item in the list */ +interface ErrorLogItemProps { + name: string; + size: number; + modified: number; + isSelected: boolean; + onClick: () => void; +} - // Don't show while status is loading or if proxy not running - if (isStatusLoading) { - return null; - } +function ErrorLogItem({ name, size, modified, isSelected, onClick }: ErrorLogItemProps) { + const { endpoint, timestamp } = parseErrorLogName(name); - if (!status?.running) { - return null; - } - - if (isLoading) { - return ( -
-
- - + return ( + + ); +} + +export function ErrorLogsMonitor({ demo = false }: { demo?: boolean } = {}) { + const { data: status, isLoading: isStatusLoading } = useCliproxyStatus(); + const { + data: logs, + isLoading, + error, + } = useCliproxyErrorLogs(demo ? false : (status?.running ?? false)); + // Use demo data or real data + const displayLogs = demo ? DEMO_ERROR_LOGS : logs; + + // Compute default selection (first log name or null) + const defaultLogName = useMemo(() => displayLogs?.[0]?.name ?? null, [displayLogs]); + + // 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; + + // Non-demo mode guards + if (!demo) { + if (isStatusLoading) return null; + if (!status?.running) return null; + if (isLoading) { + return ( +
+
+ + +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ); + } + if (!logs || logs.length === 0) return null; } - // Don't show if no errors (good state) - if (!logs || logs.length === 0) { - return null; - } - - const errorCount = logs.length; + const errorCount = displayLogs?.length ?? 0; return (
- {/* Header with warning styling */} + {/* Header */}
-
- -
- Error Logs + + Error Logs {errorCount} failed request{errorCount !== 1 ? 's' : ''} + {demo && ( + + + DEMO + + )}
@@ -134,78 +301,36 @@ export function ErrorLogsMonitor() {
- {/* Error logs list */} - -
- {logs.slice(0, 10).map((log) => { - const isExpanded = expandedLog === log.name; - const { endpoint, timestamp } = parseErrorLogName(log.name); - - return ( -
- - - {/* Expandable content */} - {isExpanded && ( -
- -
- )} + {/* Split View: List (left) + Content (right) */} +
+ {/* Left Panel: Log List */} +
+ +
+ {displayLogs?.slice(0, 10).map((log) => ( + setSelectedLog(log.name)} + /> + ))} +
+ {(displayLogs?.length ?? 0) > 10 && ( +
+ Showing 10 of {displayLogs?.length} logs
- ); - })} + )} +
- {/* Show more indicator */} - {logs.length > 10 && ( -
- Showing 10 of {logs.length} error logs -
- )} - + {/* Right Panel: Log Content */} + +
- {/* Footer hint */} + {/* Footer error */} {error && (
{error.message}