import React, { useEffect, useState, useRef } from 'react'; import { Terminal, Trash2, Play, Square } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { LogEntry } from '../types'; const Logs: React.FC = () => { const { t } = useTranslation(); const { q } = useAppContext(); const [logs, setLogs] = useState([]); const [codeMap, setCodeMap] = useState>({}); const [isStreaming, setIsStreaming] = useState(true); const [showRaw, setShowRaw] = useState(false); const logEndRef = useRef(null); const abortControllerRef = useRef(null); const loadRecent = async () => { try { const r = await fetch(`/webui/api/logs/recent${q ? `${q}&limit=10` : '?limit=10'}`); if (!r.ok) return; const j = await r.json(); if (Array.isArray(j.logs)) { setLogs(j.logs.map(normalizeLog)); } } catch (e) { console.error('L0096', e); } }; const startStreaming = async () => { if (abortControllerRef.current) abortControllerRef.current.abort(); abortControllerRef.current = new AbortController(); try { const response = await fetch(`/webui/api/logs/stream${q}`, { signal: abortControllerRef.current.signal, }); if (!response.body) return; const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter(line => line.trim()); lines.forEach(line => { try { const log = normalizeLog(JSON.parse(line)); setLogs(prev => [...prev.slice(-1000), log]); } catch (e) { // Fallback for non-JSON logs setLogs(prev => [...prev.slice(-1000), normalizeLog({ time: new Date().toISOString(), level: 'INFO', msg: line })]); } }); } } catch (e: any) { if (e.name !== 'AbortError') { console.error('L0097', e); } } }; const loadCodeMap = async () => { try { const paths = [`/webui/log-codes.json${q}`, '/log-codes.json']; for (const p of paths) { const r = await fetch(p); if (!r.ok) continue; const j = await r.json(); if (Array.isArray(j?.items)) { const m: Record = {}; j.items.forEach((it: any) => { if (typeof it?.code === 'number' && typeof it?.text === 'string') { m[it.code] = it.text; } }); setCodeMap(m); return; } } } catch { setCodeMap({}); } }; useEffect(() => { loadCodeMap(); }, [q]); useEffect(() => { loadRecent(); if (isStreaming) { startStreaming(); } else { if (abortControllerRef.current) abortControllerRef.current.abort(); } return () => { if (abortControllerRef.current) abortControllerRef.current.abort(); }; }, [isStreaming, q]); useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); const clearLogs = () => setLogs([]); const normalizeLog = (v: any): LogEntry => ({ time: typeof v?.time === 'string' && v.time ? v.time : (typeof v?.timestamp === 'string' && v.timestamp ? v.timestamp : new Date().toISOString()), level: typeof v?.level === 'string' && v.level ? v.level : 'INFO', code: typeof v?.code === 'number' ? v.code : undefined, msg: typeof v?.msg === 'string' ? v.msg : (typeof v?.message === 'string' ? v.message : JSON.stringify(v)), __raw: JSON.stringify(v), ...v, }); const toCode = (v: any): number | undefined => { if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v; if (typeof v === 'string') { if (/^L\d{4}$/.test(v)) return Number(v.slice(1)); const n = Number(v); if (Number.isFinite(n) && n > 0) return n; } return undefined; }; const decode = (v: any) => { const c = toCode(v); if (!c) return v; return codeMap[c] || v; }; const formatTime = (raw: string) => { try { if (!raw) return '--:--:--'; if (raw.includes('T')) { const right = raw.split('T')[1] || ''; return (right.split('.')[0] || right).trim() || '--:--:--'; } return raw; } catch { return '--:--:--'; } }; const renderReadable = (log: LogEntry) => { const keys = Object.keys(log).filter(k => !['time', 'level', 'msg', '__raw'].includes(k)); const core = `${log.msg}`; if (keys.length === 0) return core; const extra = keys.map(k => `${k}=${JSON.stringify((log as any)[k])}`).join(' ยท '); return `${core} | ${extra}`; }; const getLevelColor = (level: string) => { switch ((level || 'INFO').toUpperCase()) { case 'ERROR': return 'text-red-400'; case 'WARN': return 'text-amber-400'; case 'DEBUG': return 'text-blue-400'; default: return 'text-emerald-400'; } }; return (

{t('logs')}

{isStreaming ? t('live') : t('paused')}
{t('systemLog')}
{logs.length} {t('entries')}
{logs.length === 0 ? (

{t('waitingForLogs')}

) : showRaw ? (
{logs.map((log, i) => (
{log.__raw || JSON.stringify(log)}
))}
) : ( {logs.map((log, i) => { const lvl = (log.level || 'INFO').toUpperCase(); const rawCode = (log as any).code ?? (log as any).message ?? log.msg ?? ''; const message = String(decode(rawCode) || ''); const errRaw = (log as any).message || (log as any).error || (lvl === 'ERROR' ? rawCode : ''); const errText = String(decode(errRaw) || ''); const caller = (log as any).caller || (log as any).source || ''; const code = toCode(rawCode); return ( ); })}
{t('time')} {t('level')} {t('message')} {t('error')} {t('codeCaller')}
{formatTime(log.time)} {lvl} {message} {errText} {code ? `${code} | ${caller}` : caller}
)}
); }; export default Logs;