import { useEffect, useRef, useState } from 'react' import { env } from '@/config/env' import { cn } from '@/lib/utils' interface LogLine { time: string level: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' logger: string request_id: string message: string [key: string]: unknown } const LEVEL_STYLES: Record = { DEBUG: 'text-stone-400', INFO: 'text-blue-500', WARNING: 'text-amber-500', ERROR: 'text-red-500', CRITICAL: 'text-red-700 font-bold', } const LEVEL_BG: Record = { DEBUG: '', INFO: '', WARNING: '', ERROR: 'bg-red-50 dark:bg-red-950/20', CRITICAL: 'bg-red-100 dark:bg-red-950/40', } const MAX_LINES = 500 export function SystemLogs() { const [lines, setLines] = useState([]) const [connected, setConnected] = useState(false) const [paused, setPaused] = useState(false) const [filter, setFilter] = useState('ALL') const [search, setSearch] = useState('') const bottomRef = useRef(null) const pausedRef = useRef(false) const wsRef = useRef(null) pausedRef.current = paused useEffect(() => { const wsUrl = env.apiBaseUrl.replace(/^http/, 'ws') + '/ws/logs' const ws = new WebSocket(wsUrl) wsRef.current = ws ws.onopen = () => setConnected(true) ws.onclose = () => setConnected(false) ws.onerror = () => setConnected(false) ws.onmessage = (e) => { if (pausedRef.current) return try { const line = JSON.parse(e.data) as LogLine setLines((prev) => { const next = [...prev, line] return next.length > MAX_LINES ? next.slice(next.length - MAX_LINES) : next }) } catch { // non-JSON log line — show as raw setLines((prev) => { const raw: LogLine = { time: new Date().toISOString(), level: 'INFO', logger: 'raw', request_id: '-', message: e.data, } const next = [...prev, raw] return next.length > MAX_LINES ? next.slice(next.length - MAX_LINES) : next }) } } return () => ws.close() }, []) // Auto-scroll when not paused useEffect(() => { if (!paused) bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [lines, paused]) const visible = lines.filter((l) => { if (filter !== 'ALL' && l.level !== filter) return false if (search && !l.message.toLowerCase().includes(search.toLowerCase()) && !l.logger.toLowerCase().includes(search.toLowerCase())) return false return true }) function clearLogs() { setLines([]) } return (
{/* Controls */}
{connected ? 'Live' : 'Disconnected'}
setSearch(e.target.value)} placeholder="Filter logs…" className="h-8 flex-1 rounded-lg border border-surface-subtle bg-white px-3 text-xs dark:bg-stone-900 min-w-0" />
{/* Log terminal */}
{visible.length === 0 ? (

{connected ? 'Waiting for log events…' : 'WebSocket not connected — check that the backend is running.'}

) : ( visible.map((l, i) => (
{new Date(l.time).toLocaleTimeString()} {l.level} {l.logger} {l.request_id !== '-' && ( [{l.request_id.slice(0, 8)}] )} {l.message}
)) )}

{visible.length} lines shown (max {MAX_LINES})

) }