'use client'; import { useState, useEffect, useCallback } from 'react'; import { Download, RefreshCw, Filter, Activity, AlertTriangle, Info, Bug, Server, } from 'lucide-react'; const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; const LOGS_API_URL = `${API_URL}/api/v1/logs`; // ─── Types ──────────────────────────────────────────────────────────────────── interface LogEntry { timestamp: string; service: string; level: string; context: string; message: string; reqId: string; req_method: string; req_url: string; res_status: string; response_time_ms: string; error: string; } interface LogsResponse { total: number; query: string; range: { from: string; to: string }; logs: LogEntry[]; } interface Filters { service: string; level: string; search: string; startDate: string; endDate: string; limit: string; } // ─── Helpers ────────────────────────────────────────────────────────────────── const LEVEL_STYLES: Record = { error: 'bg-red-100 text-red-700 border border-red-200', fatal: 'bg-red-200 text-red-900 border border-red-300', warn: 'bg-yellow-100 text-yellow-700 border border-yellow-200', info: 'bg-blue-100 text-blue-700 border border-blue-200', debug: 'bg-gray-100 text-gray-600 border border-gray-200', trace: 'bg-purple-100 text-purple-700 border border-purple-200', }; const LEVEL_ROW_BG: Record = { error: 'bg-red-50', fatal: 'bg-red-100', warn: 'bg-yellow-50', info: '', debug: '', trace: '', }; function LevelBadge({ level }: { level: string }) { const style = LEVEL_STYLES[level] || 'bg-gray-100 text-gray-600'; return ( {level} ); } function StatCard({ label, value, icon: Icon, color, }: { label: string; value: number | string; icon: any; color: string; }) { return (

{value}

{label}

); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function AdminLogsPage() { const [logs, setLogs] = useState([]); const [services, setServices] = useState([]); const [loading, setLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false); const [error, setError] = useState(null); const [total, setTotal] = useState(0); const [expandedRow, setExpandedRow] = useState(null); const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); const [filters, setFilters] = useState({ service: 'all', level: 'all', search: '', startDate: oneHourAgo.toISOString().slice(0, 16), endDate: now.toISOString().slice(0, 16), limit: '500', }); // Load available services useEffect(() => { fetch(`${LOGS_API_URL}/services`) .then(r => r.json()) .then(d => setServices(d.services || [])) .catch(() => {}); }, []); const buildQueryString = useCallback( (fmt?: string) => { const params = new URLSearchParams(); if (filters.service !== 'all') params.set('service', filters.service); if (filters.level !== 'all') params.set('level', filters.level); if (filters.search) params.set('search', filters.search); if (filters.startDate) params.set('start', new Date(filters.startDate).toISOString()); if (filters.endDate) params.set('end', new Date(filters.endDate).toISOString()); params.set('limit', filters.limit); if (fmt) params.set('format', fmt); return params.toString(); }, [filters], ); const fetchLogs = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch( `${LOGS_API_URL}/export?${buildQueryString('json')}`, ); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `HTTP ${res.status}`); } const data: LogsResponse = await res.json(); setLogs(data.logs || []); setTotal(data.total || 0); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }, [buildQueryString]); useEffect(() => { fetchLogs(); }, []); const handleExport = async (format: 'json' | 'csv') => { setExportLoading(true); try { const res = await fetch( `${LOGS_API_URL}/export?${buildQueryString(format)}`, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `xpeditis-logs-${new Date().toISOString().slice(0, 10)}.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err: any) { setError(err.message); } finally { setExportLoading(false); } }; // Stats const countByLevel = (level: string) => logs.filter(l => l.level === level).length; const setFilter = (key: keyof Filters, value: string) => setFilters(prev => ({ ...prev, [key]: value })); return (
{/* Header */}

Logs système

Visualisation et export des logs applicatifs en temps réel

{/* Stats */}
{/* Filters */}

Filtres

{/* Service */}
{/* Level */}
{/* Search */}
setFilter('search', e.target.value)} onKeyDown={e => e.key === 'Enter' && fetchLogs()} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#34CCCD] focus:border-[#34CCCD] focus:outline-none" />
{/* Start */}
setFilter('startDate', e.target.value)} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#34CCCD] focus:border-[#34CCCD] focus:outline-none" />
{/* End */}
setFilter('endDate', e.target.value)} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#34CCCD] focus:border-[#34CCCD] focus:outline-none" />
{/* Limit + Apply */}
{/* Error */} {error && (
Impossible de contacter le log-exporter : {error}
Vérifiez que le backend et le log-exporter sont démarrés.
)} {/* Table */}
{loading ? 'Chargement...' : `${total} entrée${total !== 1 ? 's' : ''}`}
{!loading && logs.length > 0 && ( Cliquer sur une ligne pour les détails )}
{loading ? (
) : logs.length === 0 && !error ? (

Aucun log trouvé pour ces filtres

) : (
{logs.map((log, i) => ( <> setExpandedRow(expandedRow === i ? null : i)} className={`cursor-pointer hover:bg-gray-50 transition-colors ${LEVEL_ROW_BG[log.level] || ''}`} > {/* Expanded detail row */} {expandedRow === i && ( )} ))}
Timestamp Service Niveau Contexte Message Req / Status
{new Date(log.timestamp).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', })} {log.service} {log.context || '—'} {log.error ? ( {log.error} ) : ( log.message )} {log.req_method && ( {log.req_method}{' '} {log.req_url}{' '} {log.res_status && ( {log.res_status} )} )}
Timestamp

{log.timestamp}

{log.reqId && (
Request ID

{log.reqId}

)} {log.response_time_ms && (
Durée

{log.response_time_ms} ms

)}
Message complet
                                {log.error
                                  ? `[ERROR] ${log.error}\n\n${log.message}`
                                  : log.message}
                              
)}
); }