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 { useUI } from '../context/UIContext'; import EmptyState from '../components/data-display/EmptyState'; import { LogEntry } from '../types'; import { formatLocalTime } from '../utils/time'; import { Button, FixedButton } from '../components/ui/Button'; import PageHeader from '../components/layout/PageHeader'; import ToolbarRow from '../components/layout/ToolbarRow'; import { useLogStream } from '../hooks/useLogStream'; const Logs: React.FC = () => { const { t } = useTranslation(); const ui = useUI(); const { q } = useAppContext(); const { logs, isStreaming, setIsStreaming, clearLogs: hookClearLogs } = useLogStream({ q }); const [codeMap, setCodeMap] = useState>({}); const [showRaw, setShowRaw] = useState(false); const logEndRef = useRef(null); 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(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); const clearLogs = async () => { const ok = await ui.confirmDialog({ title: t('logsClearConfirmTitle'), message: t('logsClearConfirmMessage'), danger: true, confirmText: t('clear'), }); if (!ok) return; hookClearLogs(); }; 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 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 'ui-text-danger'; case 'WARN': return 'ui-code-warning'; case 'DEBUG': return 'ui-icon-info'; default: return 'ui-icon-success'; } }; return (
{t('logs')}
{isStreaming ? t('live') : t('paused')}
} titleClassName="ui-text-primary flex items-center gap-3" actions={ } />
{t('systemLog')}
{logs.length} {t('entries')}
{logs.length === 0 ? ( } message={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')}
{formatLocalTime(log.time)} {lvl} {message} {errText} {code ? `${code} | ${caller}` : caller}
)}
); }; export default Logs;