"use client"; /** * Console Log Viewer — Real-time application log viewer. * * Displays structured application logs from the server with a terminal-like UI. * Polls the backend API every 5 seconds. Shows logs from the last 1 hour. * Supports level filtering, text search, auto-scroll, and copy-to-clipboard. */ import { useState, useEffect, useRef, useCallback } from "react"; interface LogEntry { timestamp: string; level: string; component?: string; module?: string; message?: string; msg?: string; correlationId?: string; [key: string]: unknown; } const LEVEL_COLORS: Record = { debug: "text-gray-400", trace: "text-gray-500", info: "text-cyan-400", warn: "text-yellow-400", error: "text-red-400", fatal: "text-fuchsia-400", }; const LEVEL_BG: Record = { debug: "bg-gray-500/10 border-gray-500/20", trace: "bg-gray-500/10 border-gray-500/20", info: "bg-cyan-500/10 border-cyan-500/20", warn: "bg-yellow-500/10 border-yellow-500/20", error: "bg-red-500/10 border-red-500/20", fatal: "bg-fuchsia-500/10 border-fuchsia-500/20", }; const POLL_INTERVAL = 5000; // 5 seconds export default function ConsoleLogViewer() { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [levelFilter, setLevelFilter] = useState("all"); const [searchText, setSearchText] = useState(""); const [autoScroll, setAutoScroll] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [copiedIdx, setCopiedIdx] = useState(null); const scrollRef = useRef(null); const fetchLogs = useCallback(async () => { try { const params = new URLSearchParams(); if (levelFilter !== "all") params.set("level", levelFilter); params.set("limit", "500"); const res = await fetch(`/api/logs/console?${params.toString()}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data: LogEntry[] = await res.json(); setLogs(data); setLastUpdated(new Date()); setError(null); } catch (err: any) { setError(err.message || "Failed to fetch logs"); } finally { setLoading(false); } }, [levelFilter]); // Initial fetch + polling useEffect(() => { fetchLogs(); const interval = setInterval(fetchLogs, POLL_INTERVAL); return () => clearInterval(interval); }, [fetchLogs]); // Auto-scroll to bottom on new logs useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logs, autoScroll]); const handleCopy = (entry: LogEntry, idx: number) => { const text = JSON.stringify(entry, null, 2); navigator.clipboard.writeText(text).then(() => { setCopiedIdx(idx); setTimeout(() => setCopiedIdx(null), 2000); }); }; const formatTime = (ts: string) => { try { const d = new Date(ts); return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3, }); } catch { return ts; } }; const getText = (entry: LogEntry) => entry.msg || entry.message || ""; const getComponent = (entry: LogEntry) => entry.component || entry.module || ""; // Apply text search filter const filteredLogs = searchText ? logs.filter((entry) => { const full = JSON.stringify(entry).toLowerCase(); return full.includes(searchText.toLowerCase()); }) : logs; return (
{/* Toolbar */}
{/* Level filter */} {/* Search */} setSearchText(e.target.value)} aria-label="Search log entries" className="flex-1 min-w-[200px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]" /> {/* Auto-scroll toggle */} {/* Refresh */} {/* Status */}
{filteredLogs.length} entries Last 1h {lastUpdated && ( <> Updated {lastUpdated.toLocaleTimeString()} )}
{/* Error */} {error && (
error {error} — Make sure the application is writing logs to file (LOG_TO_FILE=true)
)} {/* Console output */}
{/* Header bar */}
OmniRoute — Application Console
{/* Log entries */}
{filteredLogs.length === 0 && !loading ? (
terminal

No log entries found

Ensure LOG_TO_FILE=true is set in your .env file

) : ( filteredLogs.map((entry, idx) => { const level = (entry.level || "info").toLowerCase(); const colorClass = LEVEL_COLORS[level] || LEVEL_COLORS.info; const bgClass = LEVEL_BG[level] || ""; const comp = getComponent(entry); const msg = getText(entry); return (
{/* Timestamp */} {formatTime(entry.timestamp)} {/* Level badge */} {level.padEnd(5)} {/* Component */} {comp && [{comp}]} {/* Message */} {msg} {/* Extra meta */} {entry.correlationId && ( cid:{entry.correlationId.slice(0, 8)} )} {/* Copy button */}
); }) )} {loading && filteredLogs.length === 0 && (
progress_activity Loading logs...
)}
); }