import { useEffect, useLayoutEffect, useState, useCallback, useRef } from "react"; import { FileText, RefreshCw } from "lucide-react"; import { api } from "@/lib/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { FilterGroup, Segmented } from "@/components/ui/segmented"; import { useI18n } from "@/i18n"; import { usePageHeader } from "@/contexts/usePageHeader"; const FILES = ["agent", "errors", "gateway"] as const; const LEVELS = ["ALL", "DEBUG", "INFO", "WARNING", "ERROR"] as const; const COMPONENTS = ["all", "gateway", "agent", "tools", "cli", "cron"] as const; const LINE_COUNTS = [50, 100, 200, 500] as const; function classifyLine(line: string): "error" | "warning" | "info" | "debug" { const upper = line.toUpperCase(); if ( upper.includes("ERROR") || upper.includes("CRITICAL") || upper.includes("FATAL") ) return "error"; if (upper.includes("WARNING") || upper.includes("WARN")) return "warning"; if (upper.includes("DEBUG")) return "debug"; return "info"; } const LINE_COLORS: Record = { error: "text-destructive", warning: "text-warning", info: "text-foreground", debug: "text-muted-foreground/60", }; const toOptions = (values: readonly T[]) => values.map((v) => ({ value: v, label: v })); export default function LogsPage() { const [file, setFile] = useState<(typeof FILES)[number]>("agent"); const [level, setLevel] = useState<(typeof LEVELS)[number]>("ALL"); const [component, setComponent] = useState<(typeof COMPONENTS)[number]>("all"); const [lineCount, setLineCount] = useState<(typeof LINE_COUNTS)[number]>(100); const [autoRefresh, setAutoRefresh] = useState(false); const [lines, setLines] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const scrollRef = useRef(null); const { t } = useI18n(); const { setAfterTitle, setEnd } = usePageHeader(); const fetchLogs = useCallback(() => { setLoading(true); setError(null); api .getLogs({ file, lines: lineCount, level, component }) .then((resp) => { setLines(resp.lines); setTimeout(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, 50); }) .catch((err) => setError(String(err))) .finally(() => setLoading(false)); }, [file, lineCount, level, component]); useLayoutEffect(() => { setAfterTitle( {loading && (
)} {file} · {level} · {component} , ); setEnd(
{autoRefresh && ( {t.common.live} )}
, ); return () => { setAfterTitle(null); setEnd(null); }; }, [ autoRefresh, component, file, level, loading, setAfterTitle, setEnd, t.common.live, t.common.refresh, t.logs.autoRefresh, fetchLogs, ]); useEffect(() => { fetchLogs(); }, [fetchLogs]); useEffect(() => { if (!autoRefresh) return; const interval = setInterval(fetchLogs, 5000); return () => clearInterval(interval); }, [autoRefresh, fetchLogs]); return (
{/* ═══════════════ Filter toolbar ═══════════════ */}
setLineCount(Number(v) as (typeof LINE_COUNTS)[number]) } options={LINE_COUNTS.map((n) => ({ value: String(n), label: String(n), }))} />
{/* ═══════════════ Log viewer ═══════════════ */} {file}.log {error && (

{error}

)}
{lines.length === 0 && !loading && (

{t.logs.noLogLines}

)} {lines.map((line, i) => { const cls = classifyLine(line); return (
{line}
); })}
); }