"use client"; import { useState, useEffect, useCallback, useMemo, useRef, forwardRef, useImperativeHandle, } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import Card from "./Card"; import RequestLoggerDetail from "./RequestLoggerDetail"; import { copyToClipboard } from "@/shared/utils/clipboard"; import { PROVIDER_COLORS, getHttpStatusStyle as getStatusStyle, getProtocolColor, } from "@/shared/constants/colors"; import { formatTime, formatDuration, maskAccount, stableAccountSuffix, formatApiKeyLabel, } from "@/shared/utils/formatting"; import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; import { computeLogsSignature, shouldAutoRefresh, shouldTriggerInfiniteScroll, } from "./requestLoggerSignature"; import { DEFAULT_REFRESH_INTERVAL_SEC, clampRefreshIntervalSec, readSavedRefreshIntervalSec, writeSavedRefreshIntervalSec, } from "./requestLoggerPreferences"; import { LOG_TABLE_CLASS, LOG_TABLE_HEAD_CLASS, LOG_TABLE_HEADER_BG_STYLE, LOG_TABLE_HEADER_CELL_CLASS, LOG_TABLE_HEADER_CELL_RIGHT_CLASS, LOG_TABLE_ROW_CLASS, } from "./logTableStyles"; // Number of call-log rows fetched per page. The viewer grows its window by this // amount on "Load more" / infinite scroll so users can browse past the first // page (previously hardcoded to a single 300-row window). See #2565. // Reduced from 300 → 50 to avoid browser freeze and network saturation. const PAGE_SIZE = 50; function getLogTotalTokens(log) { return (log?.tokens?.in || 0) + (log?.tokens?.out || 0); } function getLogTps(log): number { const tokensOut = log?.tokens?.out || 0; const durationMs = log?.duration || 0; if (tokensOut <= 0 || durationMs <= 0) return 0; return tokensOut / (durationMs / 1000); } function formatTps(tps: number): string { if (tps <= 0) return "—"; if (tps >= 100) return Math.round(tps).toLocaleString(); return tps.toFixed(1); } function getCacheSourceMeta(cacheSource: unknown) { if (cacheSource === "semantic") { return { key: "semantic", className: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-500/30", }; } return { key: "upstream", className: "bg-sky-500/15 text-sky-700 dark:text-sky-300 border border-sky-500/30", }; } export interface RequestLoggerV2Handle { openDetail: (logEntry: any) => void; getSortedLogs: () => any[]; } const RequestLoggerV2 = forwardRef( (props, ref) => { const { initialSelectedId } = props as any; const t = useTranslations("requestLogger"); const tCache = useTranslations("cache"); const { emailsVisible } = useEmailPrivacyStore(); // Get translated status filters const statusFilters = useMemo( () => [ { key: "all", label: t("statusFilters.all"), icon: "" }, { key: "error", label: t("statusFilters.error"), icon: "error" }, { key: "ok", label: t("statusFilters.success"), icon: "check_circle" }, { key: "combo", label: t("statusFilters.combo"), icon: "hub" }, ], [t] ); // Get translated columns const columns = useMemo( () => [ { key: "status", label: t("columns.status") }, { key: "cacheSource", label: t("columns.cacheSource") }, { key: "model", label: t("columns.model") }, { key: "requestedModel", label: t("columns.requested") }, { key: "provider", label: t("columns.provider") }, { key: "protocol", label: t("columns.protocol") }, { key: "account", label: t("columns.account") }, { key: "apiKey", label: t("columns.apiKey") }, { key: "combo", label: t("columns.combo") }, { key: "tokens", label: t("columns.tokens") }, { key: "tps", label: t("columns.tps") }, { key: "duration", label: t("columns.duration") }, { key: "time", label: t("columns.time") }, ], [t] ); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [recording, setRecording] = useState(true); const [search, setSearch] = useState(""); const [activeFilter, setActiveFilter] = useState("all"); const [selectedModel, setSelectedModel] = useState(""); const [selectedAccount, setSelectedAccount] = useState(""); const [selectedProvider, setSelectedProvider] = useState(""); const [selectedApiKey, setSelectedApiKey] = useState(""); const [sortBy, setSortBy] = useState("newest"); const [selectedLog, setSelectedLog] = useState(null); const [correlationIdFilter, setCorrelationIdFilter] = useState(""); const [hoveredCid, setHoveredCid] = useState(null); const [groupedView, setGroupedView] = useState(false); const [detailLoading, setDetailLoading] = useState(false); // Column sort toggle: clicking a column header toggles asc/desc const columnSortMap = { status: { desc: "status_desc", asc: "status_asc" }, model: { desc: "model_desc", asc: "model_asc" }, tokens: { desc: "tokens_desc", asc: "tokens_asc" }, tps: { desc: "tps_desc", asc: "tps_asc" }, duration: { desc: "duration_desc", asc: "duration_asc" }, time: { desc: "newest", asc: "oldest" }, }; const toggleSort = useCallback((column: string) => { const mapping = columnSortMap[column as keyof typeof columnSortMap]; if (!mapping) return; setSortBy((prev) => { if (prev === mapping.desc) return mapping.asc; return mapping.desc; }); }, []); const getSortIndicator = useCallback( (column: string) => { const mapping = columnSortMap[column as keyof typeof columnSortMap]; if (!mapping) return ""; if (sortBy === mapping.desc) return " ↓"; if (sortBy === mapping.asc) return " ↑"; return ""; }, [sortBy] ); const [detailData, setDetailData] = useState(null); const [detailLoggingEnabled, setDetailLoggingEnabled] = useState(false); const [detailLoggingLoading, setDetailLoggingLoading] = useState(false); const [limit, setLimit] = useState(PAGE_SIZE); const [hasMore, setHasMore] = useState(false); const [refreshIntervalSec, setRefreshIntervalSec] = useState(DEFAULT_REFRESH_INTERVAL_SEC); const intervalRef = useRef(null); const refreshIntervalSecRef = useRef(DEFAULT_REFRESH_INTERVAL_SEC); const detailRequestRef = useRef(""); const hasLoadedRef = useRef(false); const logsSignatureRef = useRef(""); const scrollContainerRef = useRef(null); const loadMoreSentinelRef = useRef(null); const hasScrolledRef = useRef(false); const [providerNodes, setProviderNodes] = useState([]); const visibleRef = useRef(true); const [visibleColumns, setVisibleColumns] = useState(() => { const defaultVisible = Object.fromEntries(columns.map((c) => [c.key, true])); if (globalThis.window === undefined) return defaultVisible; try { const saved = localStorage.getItem("loggerVisibleColumns"); return saved ? { ...defaultVisible, ...JSON.parse(saved) } : defaultVisible; } catch { return defaultVisible; } }); const toggleColumn = useCallback((key) => { setVisibleColumns((prev) => { const next = { ...prev, [key]: !prev[key] }; try { localStorage.setItem("loggerVisibleColumns", JSON.stringify(next)); } catch {} return next; }); }, []); useEffect(() => { const saved = readSavedRefreshIntervalSec(); refreshIntervalSecRef.current = saved; setRefreshIntervalSec(saved); }, []); useEffect(() => { refreshIntervalSecRef.current = refreshIntervalSec; }, [refreshIntervalSec]); const updateRefreshIntervalSec = useCallback( (valueOrUpdater: number | ((current: number) => number)) => { const current = refreshIntervalSecRef.current; const rawValue = typeof valueOrUpdater === "function" ? valueOrUpdater(current) : valueOrUpdater; const next = clampRefreshIntervalSec(rawValue); refreshIntervalSecRef.current = next; writeSavedRefreshIntervalSec(next); setRefreshIntervalSec(next); }, [] ); const fetchLogs = useCallback( async (showLoading = false) => { if (showLoading) setLoading(true); try { const params = new URLSearchParams(); if (search) params.set("search", search); if (activeFilter === "error") params.set("status", "error"); if (activeFilter === "ok") params.set("status", "ok"); if (activeFilter === "combo") params.set("combo", "1"); if (selectedModel) params.set("model", selectedModel); if (selectedProvider) params.set("provider", selectedProvider); if (selectedAccount) params.set("account", selectedAccount); if (selectedApiKey) params.set("apiKey", selectedApiKey); if (correlationIdFilter) params.set("correlationId", correlationIdFilter); params.set("limit", String(limit)); const res = await fetch(`/api/usage/call-logs?${params}`); if (res.ok) { const data = await res.json(); // If the server returned a full window, more rows may exist beyond it. setHasMore(Array.isArray(data) && data.length >= limit); // Skip re-render if data hasn't changed (#1369 GPU perf). The signature // captures id + status + duration + tokens_out so in-progress updates // still re-render while identical snapshots are skipped. const sig = computeLogsSignature(data); if (sig !== logsSignatureRef.current) { logsSignatureRef.current = sig; setLogs(data); } } } catch (error) { console.error("Failed to fetch call logs:", error); } finally { if (showLoading) setLoading(false); } }, [ search, activeFilter, selectedModel, selectedAccount, selectedProvider, selectedApiKey, correlationIdFilter, limit, ] ); useEffect(() => { const showLoading = !hasLoadedRef.current; hasLoadedRef.current = true; fetchLogs(showLoading); }, [fetchLogs]); // Fetch provider nodes for display labels useEffect(() => { fetch("/api/provider-nodes") .then((r) => (r.ok ? r.json() : { nodes: [] })) .then((d) => setProviderNodes(d.nodes || [])) .catch(() => {}); }, []); useEffect(() => { fetch("/api/logs/detail?limit=1") .then(async (res) => { if (!res.ok) return null; return await res.json(); }) .then((data) => { if (!data) return; setDetailLoggingEnabled(data.enabled === true); }) .catch(() => {}); }, []); // Visibility-aware auto-refresh: pause polling when tab is hidden or user has // scrolled past the first page, or when a detail modal is open. useEffect(() => { const onVisibility = () => { const isVisible = document.visibilityState === "visible"; visibleRef.current = isVisible; if (isVisible && !selectedLog && shouldAutoRefresh(recording, limit, PAGE_SIZE)) { fetchLogs(false); } }; // #4133: re-arm on window focus. Embedded / proxied hosts (Docker dashboard // wrappers, webviews) can fire a one-shot `visibilitychange` → hidden and // then keep reporting "hidden" — or recover without firing the event again — // which left `visibleRef` stuck `false` and froze auto-refresh permanently // (the "still not refreshing on 3.8.28, works on 3.8.24" report). A window // `focus` is a reliable signal the page is actively viewed, so re-arm and // poll. A genuinely backgrounded tab never receives focus, so this does not // defeat the perf pause. const onFocus = () => { visibleRef.current = true; if (!selectedLog && shouldAutoRefresh(recording, limit, PAGE_SIZE)) { fetchLogs(false); } }; document.addEventListener("visibilitychange", onVisibility); window.addEventListener("focus", onFocus); return () => { document.removeEventListener("visibilitychange", onVisibility); window.removeEventListener("focus", onFocus); }; }, [recording, limit, fetchLogs, selectedLog]); useEffect(() => { if (intervalRef.current) clearInterval(intervalRef.current); if (!selectedLog && shouldAutoRefresh(recording, limit, PAGE_SIZE)) { intervalRef.current = setInterval(() => { // #3972/#4054/#4133: poll while the page is plausibly viewed — the // event-tracked `visibleRef` (fail-open) OR a *live* `visibilityState` // read. A real background tab has both false → pause (perf); an // embedded host that misreports "hidden" keeps polling instead of // freezing. The window-`focus` re-arm above covers a host pinned // "hidden" while focused. if (visibleRef.current || document.visibilityState === "visible") { fetchLogs(false); } }, refreshIntervalSec * 1000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [recording, fetchLogs, limit, refreshIntervalSec, selectedLog]); // Reset the window back to the first page whenever the active filters change, // so switching filters doesn't keep fetching a large expanded window. useEffect(() => { setLimit(PAGE_SIZE); // #4269: a filter change is a fresh first-page view — re-arm the ghost-load-more // guard so auto-refresh resumes until the user scrolls again. hasScrolledRef.current = false; }, [ search, activeFilter, selectedModel, selectedAccount, selectedProvider, selectedApiKey, correlationIdFilter, ]); const loadMore = useCallback(() => { setLimit((prev) => prev + PAGE_SIZE); }, []); // #4269: record the first genuine user scroll of the log list. Until then, the // infinite-scroll observer below must NOT grow the window (see // shouldTriggerInfiniteScroll) — otherwise a sentinel that is already visible on // mount fires a "ghost" loadMore and permanently pauses auto-refresh. useEffect(() => { const root = scrollContainerRef.current; if (!root) return; const onScroll = () => { if (root.scrollTop > 0) hasScrolledRef.current = true; }; root.addEventListener("scroll", onScroll, { passive: true }); return () => root.removeEventListener("scroll", onScroll); }, []); // Infinite scroll: grow the window when the sentinel near the bottom of the // scroll container becomes visible — but only after a real user scroll (#4269). useEffect(() => { const sentinel = loadMoreSentinelRef.current; const root = scrollContainerRef.current; if (!sentinel || !hasMore) return; const observer = new IntersectionObserver( (entries) => { if ( shouldTriggerInfiniteScroll({ isIntersecting: !!entries[0]?.isIntersecting, hasMore, loading, hasScrolled: hasScrolledRef.current, }) ) { loadMore(); } }, { root, rootMargin: "200px" } ); observer.observe(sentinel); return () => observer.disconnect(); }, [hasMore, loading, loadMore]); const filteredLogs = useMemo(() => { let arr = logs; // Status filter if (activeFilter === "error") { arr = arr.filter((l) => l.status >= 400); } else if (activeFilter === "ok") { arr = arr.filter((l) => l.status >= 200 && l.status < 300); } else if (activeFilter === "combo") { arr = arr.filter((l) => l.comboName); } return arr; }, [logs, activeFilter]); // Grouped view: deduplicate by correlationId, keeping only the latest per group. // Rows without a correlationId are always shown. const dedupedLogs = useMemo(() => { if (!groupedView) return filteredLogs; const byCid = new Map(); const noCid: typeof filteredLogs = []; for (const log of filteredLogs) { const cid = log.correlationId; if (!cid) { noCid.push(log); continue; } const existing = byCid.get(cid); if ( !existing || new Date(log.timestamp).getTime() > new Date(existing.timestamp).getTime() ) { byCid.set(cid, log); } } return [...noCid, ...byCid.values()]; }, [filteredLogs, groupedView]); const sortedLogs = useMemo(() => { const arr = [...dedupedLogs]; arr.sort((a, b) => { switch (sortBy) { case "oldest": return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); case "tokens_desc": return getLogTotalTokens(b) - getLogTotalTokens(a); case "tokens_asc": return getLogTotalTokens(a) - getLogTotalTokens(b); case "duration_desc": return (b.duration || 0) - (a.duration || 0); case "duration_asc": return (a.duration || 0) - (b.duration || 0); case "tps_desc": return getLogTps(b) - getLogTps(a); case "tps_asc": return getLogTps(a) - getLogTps(b); case "status_desc": return (b.status || 0) - (a.status || 0); case "status_asc": return (a.status || 0) - (b.status || 0); case "model_asc": return (a.model || "").localeCompare(b.model || ""); case "model_desc": return (b.model || "").localeCompare(a.model || ""); case "newest": default: return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); } }); return arr; }, [dedupedLogs, sortBy]); // Group by correlationId: mark retries as children so they render indented // under the first request in each group. Compute group health: // "healed" — at least one failure followed by a success // "failed" — all attempts failed // null — single request or all succeeded const groupedLogs = useMemo(() => { const cidGroups = new Map(); for (const log of sortedLogs) { const cid = log.correlationId; if (cid) { if (!cidGroups.has(cid)) cidGroups.set(cid, []); cidGroups.get(cid).push(log); } } return sortedLogs.map((log) => { const cid = log.correlationId; if (!cid) return { ...log, isRetry: false, groupSize: 1, groupStatus: null }; const group = cidGroups.get(cid); if (!group || group.length <= 1) return { ...log, isRetry: false, groupSize: 1, groupStatus: null }; const isFirst = group[0].id === log.id; const hasFailure = group.some((g) => g.status >= 400 || g.active); const hasSuccess = group.some((g) => g.status >= 200 && g.status < 300); const groupStatus = hasFailure && hasSuccess ? "healed" : hasFailure ? "failed" : null; return { ...log, isRetry: !isFirst, groupSize: group.length, groupStatus }; }); }, [sortedLogs]); // Fetch log detail from the persisted call-log endpoint. If a deep-linked // request is still being finalized, keep the modal open and poll this same // endpoint until the row appears. const router = useRouter(); const openDetail = async (logEntry) => { // Guard: if no valid id provided, close instead of opening an empty modal if (!logEntry?.id) { try { closeDetail(); } catch {} return; } const requestToken = `${logEntry.id}:${Date.now()}:${Math.random()}`; detailRequestRef.current = requestToken; const isCurrentDetailRequest = () => detailRequestRef.current === requestToken; setSelectedLog(logEntry); try { const url = new URL(globalThis.location.href); url.searchParams.set("id", logEntry.id); router.replace(url.pathname + url.search, { scroll: false }); } catch (e) { // ignore navigation errors } setDetailLoading(true); setDetailData(null); try { const res = await fetch(`/api/logs/${logEntry.id}`, { cache: "no-store" }); if (res.ok) { const data = await res.json(); if (!isCurrentDetailRequest()) return; const dataHasPipeline = data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0; setDetailData((prev: { pipelinePayloads: any }) => ({ ...prev, ...data, pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads, })); // ensure the modal summary reflects the fetched call log summary if (data && typeof data === "object") { setSelectedLog((prev: any) => ({ ...prev, ...data, active: data.active === true, })); } } else { // A deep-linked id can legitimately 404 while the request is still // finalizing. Keep the modal open and poll /api/logs/[id] instead of // falling back to an in-memory active-request endpoint. if (!isCurrentDetailRequest()) return; if (res.status === 404) { if (logEntry.pendingLookup || logEntry.active) { setSelectedLog((prev: { method: any; path: any }) => ({ ...prev, id: logEntry.id, status: 0, method: prev?.method, path: prev?.path || "", })); setDetailData({ detailState: "pending" }); return; } try { console.warn("Log not found:", logEntry.id); } catch {} try { closeDetail(); } catch {} return; } // other errors: show a minimal error indicator by setting detailData to an error object try { const body = await res.text().catch(() => null); if (!isCurrentDetailRequest()) return; setDetailData({ error: `Failed to fetch log (status ${res.status})`, body }); } catch {} } } catch (error) { console.error("Failed to fetch log detail:", error); } finally { if (isCurrentDetailRequest()) setDetailLoading(false); } }; const closeDetail = () => { detailRequestRef.current = ""; setSelectedLog(null); setDetailData(null); try { // remove id param when closing detail const url = new URL(globalThis.location.href); url.searchParams.delete("id"); router.replace(url.pathname + url.search, { scroll: false }); } catch (e) { // ignore navigation errors } }; const sortedLogsForNav = useMemo(() => sortedLogs, [sortedLogs]); useImperativeHandle( ref, () => ({ openDetail, getSortedLogs: () => sortedLogsForNav, }), [openDetail, sortedLogsForNav] ); // If the page provided an initialSelectedId (via ?id=), open it on mount. // Guard with a ref so subsequent URL updates (close -> replace) don't re-open the modal. const initialOpenedRef = useRef(false); useEffect(() => { if (initialSelectedId && !initialOpenedRef.current) { initialOpenedRef.current = true; openDetail({ id: initialSelectedId, pendingLookup: true }) .then((r) => r) .catch((error_) => { console.error("Failed to open initial log id:", error_); }); } }, [initialSelectedId]); useEffect(() => { const isActive = selectedLog?.active === true; if (!selectedLog?.id || !isActive) return; let cancelled = false; let graceCount = 0; const interval = setInterval(async () => { if (document.visibilityState !== "visible") return; try { const res = await fetch(`/api/logs/${selectedLog.id}`, { cache: "no-store" }); if (cancelled) return; if (res.status === 404) { if (isActive) return; clearInterval(interval); return; } if (res.ok) { const data = await res.json(); const stillActive = data.active === true; setDetailData((prev: { pipelinePayloads: any }) => ({ ...prev, ...data, pipelinePayloads: data?.pipelinePayloads || prev?.pipelinePayloads, })); setSelectedLog((prev: any) => ({ ...prev, ...data, active: stillActive, })); if (!stillActive) { graceCount++; if (graceCount >= 3) clearInterval(interval); } } } catch { // keep waiting; the detail endpoint is the single source of truth } }, 1000); return () => { cancelled = true; clearInterval(interval); }; }, [selectedLog?.id, detailData?.detailState, selectedLog?.active, fetchLogs]); // Poll for related logs (same correlationId) while the detail modal is open. // This ensures newly completed retries appear without closing the modal. useEffect(() => { const cid = selectedLog?.correlationId; if (!selectedLog?.id || !cid) return; let cancelled = false; const interval = setInterval(async () => { if (document.visibilityState !== "visible") return; try { const res = await fetch(`/api/usage/call-logs?correlationId=${encodeURIComponent(cid)}`, { cache: "no-store", }); if (cancelled || !res.ok) return; const cidLogs = await res.json(); if (!Array.isArray(cidLogs) || cidLogs.length === 0) return; setLogs((prev) => { const ids = new Set(cidLogs.map((l: any) => l.id)); let changed = false; const merged = prev.map((l: any) => { const updated = cidLogs.find((c: any) => c.id === l.id); if (updated) { changed = true; return { ...l, ...updated }; } return l; }); for (const cl of cidLogs) { if (!merged.some((m: any) => m.id === cl.id)) { merged.push(cl); changed = true; } } return changed ? merged : prev; }); } catch { /* poll failed — non-critical */ } }, 3000); return () => { cancelled = true; clearInterval(interval); }; }, [selectedLog?.id, selectedLog?.correlationId]); const currentLogIndex = useMemo(() => { if (!selectedLog) return -1; return sortedLogsForNav.findIndex((l) => l.id === selectedLog.id); }, [selectedLog, sortedLogsForNav]); const handlePrev = useCallback(() => { const idx = currentLogIndex; const target = idx > 0 ? sortedLogsForNav[idx - 1] : null; if (target?.id) { openDetail(target) .then((r) => r) .catch((error_) => { console.error("Failed to open previous log id:", error_); }); } else { closeDetail(); } }, [currentLogIndex, sortedLogsForNav]); const handleNext = useCallback(() => { const idx = currentLogIndex; const target = idx >= 0 && idx < sortedLogsForNav.length - 1 ? sortedLogsForNav[idx + 1] : null; if (target?.id) { openDetail(target) .then((r) => r) .catch((error_) => { console.error("Failed to open previous log id:", error_); }); } else { closeDetail(); } }, [currentLogIndex, sortedLogsForNav]); const toggleDetailLogging = async () => { setDetailLoggingLoading(true); try { const nextEnabled = !detailLoggingEnabled; const res = await fetch("/api/logs/detail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: nextEnabled }), }); if (!res.ok) throw new Error(t("updatePipelineFailed")); setDetailLoggingEnabled(nextEnabled); } catch (error) { console.error("Failed to toggle pipeline logging:", error); } finally { setDetailLoggingLoading(false); } }; const sourceLogsForDropdowns = logs; // Unique accounts and providers for dropdowns const uniqueAccounts = useMemo( () => [ ...new Set(sourceLogsForDropdowns.map((l) => l.account).filter((a) => a && a !== "-")), ], [sourceLogsForDropdowns] ); const uniqueModels = useMemo( () => [ ...new Set( sourceLogsForDropdowns.flatMap((l) => [l.model, l.requestedModel]).filter(Boolean) ), ].sort(), [sourceLogsForDropdowns] ); const uniqueProviders = useMemo( () => [ ...new Set(sourceLogsForDropdowns.map((l) => l.provider).filter((p) => p && p !== "-")), ].sort(), [sourceLogsForDropdowns] ); const uniqueApiKeys = useMemo( () => [ ...new Set(sourceLogsForDropdowns.map((l) => l.apiKeyId || l.apiKeyName).filter(Boolean)), ].sort(), [sourceLogsForDropdowns] ); // Stats (memoized to avoid re-computation on every render) const { totalCount, okCount, errorCount, comboCount, apiKeyCount, runningCount } = useMemo( () => ({ totalCount: filteredLogs.length, okCount: filteredLogs.filter((l) => l.status >= 200 && l.status < 300).length, errorCount: filteredLogs.filter((l) => l.status >= 400).length, comboCount: logs.filter((l) => l.comboName).length, apiKeyCount: uniqueApiKeys.length, runningCount: filteredLogs.filter((l) => l.active === true).length, }), [filteredLogs, logs, uniqueApiKeys] ); return (
{/* Header Bar */}
{/* Recording Toggle */} {/* Search */}
search setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-primary" />
{/* Correlation ID Filter */}
tag setCorrelationIdFilter(e.target.value)} className="w-full pl-9 pr-3 py-2 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary font-mono placeholder:text-text-muted focus:outline-none focus:border-primary" />
{/* Group by CID toggle */} {/* Provider Dropdown */} {/* Model Dropdown */} {/* Account Dropdown */} {/* API Key Dropdown */} {/* Stats */}
{totalCount} {t("total")} {runningCount > 0 && ( {runningCount} running )} {okCount} {t("ok")} {errorCount > 0 && ( {errorCount} {t("err")} )} {comboCount > 0 && ( {comboCount} {t("combo")} )} {apiKeyCount > 0 && ( {apiKeyCount} {t("keys")} )} {sortedLogs.length} {t("shown")}
{/* Sort Dropdown */} {/* Refresh interval */}
{ const v = Number.parseInt(e.target.value, 10); if (!Number.isNaN(v)) updateRefreshIntervalSec(v); }} className="w-12 text-center text-[11px] bg-transparent border border-border rounded px-1 py-0.5 text-text-primary [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" title="Auto-refresh interval in seconds" /> s
{/* Refresh */}
{/* Quick Filters */}
{/* Status Filters */} {statusFilters.map((f) => ( ))} {/* Divider */} {uniqueProviders.length > 0 && } {/* Dynamic Provider Quick Filters (from data) */} {uniqueProviders.map((p) => { const compatLabel = getProviderDisplayLabel(p, providerNodes); const pc = PROVIDER_COLORS[p] || { bg: "#374151", text: "#fff", label: compatLabel || p.toUpperCase(), }; const displayLabel = compatLabel || pc.label; const isActive = selectedProvider === p; return ( ); })}
{/* Column Visibility Toggles */}
{t("columnsLabel")} {columns.map((col) => ( ))}
{/* Table */}
{loading && logs.length === 0 ? (
{t("loadingLogs")}
) : logs.length === 0 ? (
receipt_long {t("noLogs")}
) : sortedLogs.length === 0 ? (
{t("noMatchingLogs")}
) : ( {visibleColumns.status && ( )} {visibleColumns.cacheSource && ( )} {visibleColumns.model && ( )} {visibleColumns.requestedModel && ( )} {visibleColumns.provider && ( )} {visibleColumns.protocol && ( )} {visibleColumns.account && ( )} {visibleColumns.apiKey && ( )} {visibleColumns.combo && ( )} {visibleColumns.tokens && ( )} {visibleColumns.tps && ( )} {visibleColumns.duration && ( )} {visibleColumns.time && ( )} {groupedLogs.map((log) => { const isActive = log.active === true; const statusStyle = isActive ? null : getStatusStyle(log.status); const protocolKey = isActive ? null : log.sourceFormat || log.provider; const protocol = protocolKey ? getProtocolColor(protocolKey, log.provider) : null; const compatLabel = getProviderDisplayLabel(log.provider, providerNodes); const providerColor = PROVIDER_COLORS[log.provider] || { bg: "#374151", text: "#fff", label: compatLabel || (log.provider || "-").toUpperCase(), }; const providerLabel = log.providerDisplay || compatLabel || providerColor.label; const isError = !isActive && log.status >= 400; const cacheSourceMeta = getCacheSourceMeta(log.cacheSource); const isSemanticCache = cacheSourceMeta?.key === "semantic"; const accountLabel = maskAccount(log.account, emailsVisible); return ( openDetail(log)} data-cid={log.correlationId || undefined} onMouseEnter={() => log.correlationId && setHoveredCid(log.correlationId)} onMouseLeave={() => setHoveredCid(null)} className={ `cursor-pointer transition-colors ` + `${ isError ? "bg-red-500/5 hover:bg-red-500/15 dark:hover:bg-red-400/15" : "hover:bg-sky-500/10 dark:hover:bg-sky-400/10" } ` + `${log.isRetry ? "border-l-2 border-l-amber-500/50" : ""} ` + `${hoveredCid && log.correlationId === hoveredCid ? "bg-violet-500/10 dark:bg-violet-400/10 ring-1 ring-violet-500/20" : ""}` } style={ log.isRetry ? { backgroundColor: hoveredCid && log.correlationId === hoveredCid ? undefined : "rgba(245,158,11,0.03)", } : undefined } > {visibleColumns.status && ( )} {visibleColumns.cacheSource && ( )} {visibleColumns.model && ( )} {visibleColumns.requestedModel && ( )} {visibleColumns.provider && ( )} {visibleColumns.protocol && ( )} {visibleColumns.account && ( )} {visibleColumns.apiKey && ( )} {visibleColumns.combo && ( )} {visibleColumns.tokens && ( )} {visibleColumns.tps && ( )} {visibleColumns.duration && ( )} {visibleColumns.time && ( )} ); })}
toggleSort("status")} > {t("columns.status")} {getSortIndicator("status")} {t("columns.cacheSource")}{t("columns.model")}{t("columns.requested")}{t("columns.provider")}{t("columns.protocol")}{t("columns.account")}{t("columns.apiKey")}{t("columns.combo")} toggleSort("tokens")} > {t("columns.tokens")} {getSortIndicator("tokens")} toggleSort("tps")} > {t("columns.tps")} {getSortIndicator("tps")} toggleSort("duration")} > {t("columns.duration")} {getSortIndicator("duration")} toggleSort("time")} > {t("columns.time")} {getSortIndicator("time")}
{isActive ? ( ) : ( {log.status || "..."} {log.groupStatus === "healed" && !log.isRetry && log.status >= 400 && ( )} {log.isRetry && ( )} )} {isActive ? ( ) : ( {isSemanticCache ? t("semantic") : t("upstream")} )}
{log.model} {log.groupStatus === "healed" && !log.isRetry && ( healed )} {log.groupStatus === "failed" && !log.isRetry && ( failed )} {log.modelPinned && ( pinned )}
{log.correlationId && !log.isRetry && log.groupSize > 1 && (
{log.correlationId.slice(0, 12)}… · {log.groupSize} attempts
)} {log.correlationId && !log.isRetry && log.groupSize <= 1 && (
{log.correlationId.slice(0, 12)}…
)} {log.correlationId && log.isRetry && (
{log.correlationId.slice(0, 12)}…
)}
{log.requestedModel ? ( {log.requestedModel} ) : ( )} {providerLabel} {isActive ? ( ) : ( {protocol?.label || "—"} )} {accountLabel} {isActive ? ( ) : ( formatApiKeyLabel(log.apiKeyName, log.apiKeyId) )} {isActive ? ( ) : log.comboName ? ( {log.comboName} ) : ( )} {isActive ? ( ) : ( <> TI:{" "} {log.tokens?.in?.toLocaleString() || 0} | TO:{" "} {log.tokens?.out?.toLocaleString() || 0} {log.tokens?.cacheRead != null && log.tokens.cacheRead > 0 && ( <> | CR:{" "} {log.tokens.cacheRead.toLocaleString()} )} {log.tokens?.cacheWrite != null && log.tokens.cacheWrite > 0 && ( <> | CW:{" "} {log.tokens.cacheWrite.toLocaleString()} )} {log.tokens?.compressed != null && log.tokens.compressed > 0 && ( <> | ↓{log.tokens.compressed.toLocaleString()} )} )} {isActive ? ( ) : ( (() => { const tps = getLogTps(log); const color = tps <= 0 ? "text-text-muted" : tps >= 80 ? "text-emerald-600 dark:text-emerald-400" : tps >= 30 ? "text-sky-600 dark:text-sky-400" : "text-amber-600 dark:text-amber-400"; return ( {formatTps(tps)} ); })() )} {formatDuration(log.duration)} {formatTime(log.timestamp)}
)} {hasMore && sortedLogs.length > 0 && (
)}
{t("callLogsInfo", { dataDir: "{DATA_DIR}/call_logs/", retentionDays: "CALL_LOG_RETENTION_DAYS", maxEntries: "CALL_LOG_MAX_ENTRIES", })}
{/* Detail Modal */} {selectedLog && ( l.correlationId === selectedLog.correlationId) : [] } onSelectRelated={(r) => { closeDetail(); openDetail(r); }} /> )}
); } ); RequestLoggerV2.displayName = "RequestLoggerV2"; export default RequestLoggerV2;