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
This commit is contained in:
kaitranntt
2025-12-18 02:15:35 -05:00
parent 1a140e4a32
commit 5b3d56548a
6 changed files with 451 additions and 0 deletions
+84
View File
@@ -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<CliproxyErrorLog[] | null> {
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<string | null> {
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)
+73
View File
@@ -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<void> => {
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<void> => {
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
// ============================================