"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { SETTINGS_PANEL_INSET, SETTINGS_PANEL_SHELL } from "@/modules/settings/components/panel/surface"; import { queryKeys } from "@/lib/shared/query-keys"; import type { RawLogEntry, LogSource } from "@/lib/server/modules/logs/service"; import { cn } from "@/lib/utils"; type LogsApiResponse = { data: { entries: RawLogEntry[]; source: LogSource; truncated: boolean; error?: string; }; }; async function fetchLogs(source: LogSource): Promise { const res = await fetch(`/api/v1/logs?source=${source}`); if (!res.ok) throw new Error(`Failed to load ${source} logs`); const json = (await res.json()) as LogsApiResponse; return json.data; } const TABS: { id: LogSource; label: string }[] = [ { id: "homeio", label: "Homeio" }, { id: "system", label: "System" }, { id: "docker", label: "Docker" }, ]; function levelColor(level?: string) { switch (level) { case "error": return "text-status-red"; case "warn": return "text-status-amber"; case "debug": return "text-muted-foreground/60"; default: return "text-foreground/80"; } } function levelBadgeColor(level?: string) { switch (level) { case "error": return "bg-status-red/15 text-status-red"; case "warn": return "bg-status-amber/15 text-status-amber"; case "debug": return "bg-secondary text-muted-foreground"; default: return "bg-primary/10 text-primary"; } } function formatTimestamp(ts?: string) { if (!ts) return null; try { return new Date(ts).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); } catch { return ts.slice(11, 19) || null; } } function HomeioLogRow({ entry }: { entry: RawLogEntry }) { const [expanded, setExpanded] = useState(false); const ts = formatTimestamp(entry.timestamp); const hasDetail = Boolean(entry.error || entry.message); return (
hasDetail && setExpanded((p) => !p)} >
{ts && {ts}} {entry.level && ( {entry.level} )} {entry.layer && {entry.layer}} {entry.action ?? entry.raw} {entry.status && {entry.status}}
{expanded && hasDetail && (
{entry.message &&

{entry.message}

} {entry.error &&

{entry.error.message}

}
)}
); } function RawLogRow({ entry }: { entry: RawLogEntry }) { const isError = /\b(error|failed|failure|crit|emerg|alert)\b/i.test(entry.raw); const isWarn = /\b(warn|warning)\b/i.test(entry.raw); return (
{entry.raw}
); } const REFETCH_INTERVAL = 8_000; export function LogsSection() { const [activeSource, setActiveSource] = useState("homeio"); const [autoScroll, setAutoScroll] = useState(true); const scrollRef = useRef(null); const { data, isFetching, refetch, dataUpdatedAt } = useQuery({ queryKey: queryKeys.logs(activeSource), queryFn: () => fetchLogs(activeSource), refetchInterval: REFETCH_INTERVAL, staleTime: REFETCH_INTERVAL / 2, }); useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [dataUpdatedAt, autoScroll]); const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; setAutoScroll(el.scrollHeight - el.scrollTop - el.clientHeight < 40); }, []); function handleDownload() { if (!data?.entries) return; const text = data.entries.map((e) => e.raw).join("\n"); const blob = new Blob([text], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `homeio-${activeSource}-logs.txt`; a.click(); URL.revokeObjectURL(url); } const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }) : null; const sourceLabel = activeSource === "homeio" ? "Homeio Application Logs" : activeSource === "system" ? "System Journal" : "Docker Daemon Journal"; return (
{/* Tab bar */}
{TABS.map((tab) => ( ))}
{lastUpdated && ( {isFetching ? "Refreshing…" : `Updated ${lastUpdated}`} )}
{/* Log output */}
{/* Header */}
{sourceLabel}
{data?.truncated ? ( Showing last 500 lines ) : data?.entries?.length ? ( {data.entries.length} entries ) : null}
{/* Scroll area */}
{data?.error ? (
{data.error}
) : !data || data.entries.length === 0 ? (
{isFetching ? "Loading logs…" : "No log entries found"}
) : activeSource === "homeio" ? ( data.entries.map((entry, i) => ) ) : ( data.entries.map((entry, i) => ) )}
); }