import { apiPath } from "@/lib/api"; import { useCallback, useEffect, useRef, useState } from "react"; type LogEntry = { id: string; timestamp: string; level: string; kind: string; detail: string; meta: Record; }; const LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR"]; const KINDS = ["", "tool_call", "server", "export"]; const LEVEL_COLORS: Record = { ERROR: "text-red-400 bg-red-950/40", WARNING: "text-yellow-400 bg-yellow-950/40", INFO: "text-blue-300 bg-blue-950/30", DEBUG: "text-slate-500 bg-slate-900/30", }; export default function Logging() { const [entries, setEntries] = useState([]); const [total, setTotal] = useState(0); const [limit, setLimit] = useState(50); const [offset, setOffset] = useState(0); const [level, setLevel] = useState(""); const [kind, setKind] = useState(""); const [search, setSearch] = useState(""); const [sort] = useState("desc"); const [tail, setTail] = useState(false); const [loading, setLoading] = useState(false); const [showClear, setShowClear] = useState(false); const endRef = useRef(null); const containerRef = useRef(null); const [userScrolled, setUserScrolled] = useState(false); const afterIdRef = useRef(null); const debounceRef = useRef>(undefined); const fetchLogs = useCallback( async (opts: { tail?: boolean; after_id?: string } = {}) => { setLoading(true); const params = new URLSearchParams(); params.set("limit", String(limit)); params.set("offset", String(offset)); params.set("sort", sort); if (level) params.set("level", level); if (kind) params.set("kind", kind); if (search) params.set("search", search); if (opts.after_id) params.set("after_id", opts.after_id); try { const r = await fetch(apiPath(`/api/logs?${params}`)); const d = await r.json(); if (opts.tail && opts.after_id) { setEntries((prev) => [...prev, ...d.entries].slice(-200)); } else { setEntries(d.entries); } setTotal(d.total); if (d.entries.length > 0) { afterIdRef.current = d.entries[d.entries.length - 1].id; } } catch (e) { console.error("Log fetch failed", e); } finally { setLoading(false); } }, [limit, offset, level, kind, search, sort], ); useEffect(() => { fetchLogs(); }, [fetchLogs]); useEffect(() => { if (!tail) return; const iv = setInterval(() => { if (afterIdRef.current) { fetchLogs({ tail: true, after_id: afterIdRef.current }); } else { fetchLogs({ tail: true }); } }, 2000); return () => clearInterval(iv); }, [tail, fetchLogs]); useEffect(() => { if (tail && !userScrolled && endRef.current) { endRef.current.scrollIntoView({ behavior: "smooth" }); } }, [entries, tail, userScrolled]); const handleScroll = () => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const atBottom = scrollHeight - scrollTop - clientHeight < 50; setUserScrolled(!atBottom); }; const handleSearch = (val: string) => { setSearch(val); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => setSearch(val), 300); }; const handleExport = async (fmt: string) => { const params = new URLSearchParams(); params.set("format", fmt); if (level) params.set("level", level); if (kind) params.set("kind", kind); if (search) params.set("search", search); const r = await fetch(apiPath(`/api/logs/export?${params}`)); const blob = await r.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `logs.${fmt}`; a.click(); URL.revokeObjectURL(url); }; const handleClear = async () => { await fetch(apiPath("/api/logs"), { method: "DELETE" }); setShowClear(false); setEntries([]); setTotal(0); }; const totalPages = Math.ceil(total / limit); const currentPage = Math.floor(offset / limit) + 1; return (

Logs

handleSearch(e.target.value)} /> {total} entries
{entries.length === 0 && !loading && (
No log entries
)} {entries.map((e) => (
{e.timestamp.split(".")[0].split("T")[1] || e.timestamp} {e.level} {e.kind && ( [{e.kind}] )} {e.detail}
))}
Page {currentPage} of {totalPages || 1}
{showClear && (
setShowClear(false)} >
e.stopPropagation()} >

Clear all logs?

This cannot be undone. The ring buffer will be emptied.

)}
); }