diff --git a/frontend/src/api/logs.ts b/frontend/src/api/logs.ts index fe5f3ad4..1fb9bda0 100644 --- a/frontend/src/api/logs.ts +++ b/frontend/src/api/logs.ts @@ -6,8 +6,37 @@ export interface LogFile { mod_time: string; } -export interface LogContent { - lines: string[]; +export interface CaddyAccessLog { + level: string; + ts: number; + logger: string; + msg: string; + request: { + remote_ip: string; + method: string; + host: string; + uri: string; + proto: string; + }; + status: number; + duration: number; + size: number; +} + +export interface LogResponse { + filename: string; + logs: CaddyAccessLog[]; + total: number; + limit: number; + offset: number; +} + +export interface LogFilter { + search?: string; + host?: string; + status?: string; + limit?: number; + offset?: number; } export const getLogs = async (): Promise => { @@ -15,7 +44,21 @@ export const getLogs = async (): Promise => { return response.data; }; -export const getLogContent = async (filename: string, lines: number = 100): Promise => { - const response = await client.get(`/logs/${filename}?lines=${lines}`); +export const getLogContent = async (filename: string, filter: LogFilter = {}): Promise => { + const params = new URLSearchParams(); + if (filter.search) params.append('search', filter.search); + if (filter.host) params.append('host', filter.host); + if (filter.status) params.append('status', filter.status); + if (filter.limit) params.append('limit', filter.limit.toString()); + if (filter.offset) params.append('offset', filter.offset.toString()); + + const response = await client.get(`/logs/${filename}?${params.toString()}`); return response.data; }; + +export const downloadLog = (filename: string) => { + // Direct window location change to trigger download + // We need to use the base URL from the client config if possible, + // but for now we assume relative path works with the proxy setup + window.location.href = `/api/v1/logs/${filename}/download`; +}; diff --git a/frontend/src/components/LogFilters.tsx b/frontend/src/components/LogFilters.tsx new file mode 100644 index 00000000..613876f9 --- /dev/null +++ b/frontend/src/components/LogFilters.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Search, Download, RefreshCw } from 'lucide-react'; +import { Button } from './ui/Button'; + +interface LogFiltersProps { + search: string; + onSearchChange: (value: string) => void; + status: string; + onStatusChange: (value: string) => void; + host: string; + onHostChange: (value: string) => void; + onRefresh: () => void; + onDownload: () => void; + isLoading: boolean; +} + +export const LogFilters: React.FC = ({ + search, + onSearchChange, + status, + onStatusChange, + host, + onHostChange, + onRefresh, + onDownload, + isLoading +}) => { + return ( +
+
+
+ +
+ onSearchChange(e.target.value)} + className="block w-full pl-10 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white" + /> +
+ +
+ onHostChange(e.target.value)} + className="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:bg-gray-700 dark:text-white" + /> +
+ +
+ +
+ +
+ + +
+
+ ); +}; diff --git a/frontend/src/components/LogTable.tsx b/frontend/src/components/LogTable.tsx new file mode 100644 index 00000000..b97dddda --- /dev/null +++ b/frontend/src/components/LogTable.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { CaddyAccessLog } from '../api/logs'; +import { format } from 'date-fns'; + +interface LogTableProps { + logs: CaddyAccessLog[]; + isLoading: boolean; +} + +export const LogTable: React.FC = ({ logs, isLoading }) => { + if (isLoading) { + return ( +
+ Loading logs... +
+ ); + } + + if (!logs || logs.length === 0) { + return ( +
+ No logs found matching criteria. +
+ ); + } + + return ( +
+ + + + + + + + + + + + + + + {logs.map((log, idx) => ( + + + + + + + + + + + ))} + +
TimeStatusMethodHostPathIPLatencyMessage
+ {format(new Date(log.ts * 1000), 'MMM d HH:mm:ss')} + + {log.status > 0 && ( + = 500 ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : + log.status >= 400 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : + log.status >= 300 ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : + 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'}`}> + {log.status} + + )} + + {log.request?.method} + + {log.request?.host} + + {log.request?.uri} + + {log.request?.remote_ip} + + {log.duration > 0 ? (log.duration * 1000).toFixed(2) + 'ms' : ''} + + {log.msg} +
+
+ ); +};