"use client"; import Icon from "@/shared/components/Icon"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useTranslations } from "next-intl"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@omniroute/ui-kit/table"; import { DataTableShell } from "@omniroute/ui-kit/data-table-shell"; import { TableLoadingState } from "@omniroute/ui-kit/table-loading"; import { cn } from "@/shared/utils/cn"; import Badge from "./Badge"; import Button from "./Button"; import ColumnToggle from "./ColumnToggle"; import EmptyState from "./EmptyState"; import Input from "./Input"; import Select from "./Select"; import ProxyLogDetail from "./ProxyLogDetail"; import { TYPE_COLORS, LEVEL_COLORS, PROVIDER_COLORS, neutralBadge, } from "@/shared/constants/colors"; import { formatTime, formatDuration as formatLatency, maskAccount, maskSegment, truncateUrl, } from "@/shared/utils/formatting"; import { EGRESS_FAMILY_LABEL_ARGUMENT, observedEgressAddresses, } from "@/shared/network/egressAddress"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; const PROXY_COLUMN_KEYS = [ "status", "proxy", "tls", "type", "level", "provider", "upstream", "egress", "correlation", "servedBy", "target", "latency", "ip", "time", ]; /** * A proxy log row is a connection test when the writer marked it as such: * the `provider-test` level fallback (no proxy resolved) or the * `/connection-test` target suffix (a proxy was resolved, so `level` carries * the real level). Both markers are written by the connection-test route. */ function isProviderTestLog(log) { if (!log) return false; if (log.level === "provider-test") return true; return typeof log.targetUrl === "string" && log.targetUrl.endsWith("/connection-test"); } // Proxy status string → kit Badge tone. function proxyStatusTone(status: unknown) { if (status === "success") return "success" as const; if (status === "error") return "error" as const; if (status === "timeout") return "warning" as const; return "default" as const; } const DEFAULT_VISIBLE = Object.fromEntries(PROXY_COLUMN_KEYS.map((key) => [key, true])); export default function ProxyLogger() { const t = useTranslations("proxyLogger"); const tCommon = useTranslations("common"); // The family of an egress address is labelled in the proxyLog namespace, the // one the line detail uses too. const tProxyLog = useTranslations("proxyLog"); const { emailsVisible } = useEmailPrivacyStore(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [recording, setRecording] = useState(true); const [search, setSearch] = useState(""); const [activeFilter, setActiveFilter] = useState("all"); const [sourceFilter, setSourceFilter] = useState("all"); const [selectedType, setSelectedType] = useState(""); const [selectedProvider, setSelectedProvider] = useState(""); const [selectedLevel, setSelectedLevel] = useState(""); const [sortBy, setSortBy] = useState("newest"); const [selectedLog, setSelectedLog] = useState(null); const [providerNodes, setProviderNodes] = useState< Array<{ id?: string; prefix?: string; name?: string }> >([]); const intervalRef = useRef(null); const hasLoadedRef = useRef(false); const logsSignatureRef = useRef(""); const statusFilters = useMemo( () => [ { key: "all", label: t("filterAll") }, { key: "error", label: t("filterErrors"), icon: "error" }, { key: "ok", label: t("filterSuccess"), icon: "check_circle" }, { key: "timeout", label: t("filterTimeout"), icon: "timer_off" }, ], [t] ); const sourceFilters = useMemo( () => [ { key: "all", label: t("filterAll") }, { key: "traffic", label: t("filterTraffic") }, { key: "tests", label: t("filterTests") }, ], [t] ); const columns = useMemo( () => [ { key: "status", label: t("colStatus") }, { key: "proxy", label: t("colProxy") }, { key: "tls", label: t("colTls") }, { key: "type", label: t("colType") }, { key: "level", label: t("colLevel") }, { key: "provider", label: t("colProvider") }, { key: "upstream", label: t("colUpstream") }, { key: "egress", label: t("colEgress") }, { key: "correlation", label: t("colCorrelation") }, { key: "servedBy", label: t("colServedBy") }, { key: "target", label: t("colTarget") }, { key: "latency", label: t("colLatency") }, { key: "ip", label: t("colClientIp") }, { key: "time", label: t("colTime") }, ], [t] ); const [visibleColumns, setVisibleColumns] = useState(() => { if (typeof window === "undefined") return DEFAULT_VISIBLE; try { const saved = localStorage.getItem("proxyLoggerVisibleColumns"); return saved ? { ...DEFAULT_VISIBLE, ...JSON.parse(saved) } : DEFAULT_VISIBLE; } catch { return DEFAULT_VISIBLE; } }); const toggleColumn = useCallback((key) => { setVisibleColumns((prev) => { const next = { ...prev, [key]: !prev[key] }; try { localStorage.setItem("proxyLoggerVisibleColumns", JSON.stringify(next)); } catch {} return 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 === "timeout") params.set("status", "timeout"); if (selectedType) params.set("type", selectedType); if (selectedProvider) params.set("provider", selectedProvider); if (selectedLevel) params.set("level", selectedLevel); params.set("limit", "300"); const res = await fetch(`/api/usage/proxy-logs?${params}`); if (res.ok) { const data = await res.json(); // Skip re-render if data hasn't changed (#1369 GPU perf) const sig = JSON.stringify(data.map?.((l: any) => l.id) ?? []); if (sig !== logsSignatureRef.current) { logsSignatureRef.current = sig; setLogs(data); } } } catch (error) { console.error("Failed to fetch proxy logs:", error); } finally { if (showLoading) setLoading(false); } }, [search, activeFilter, selectedType, selectedProvider, selectedLevel] ); 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(() => { if (intervalRef.current) clearInterval(intervalRef.current); if (recording) { intervalRef.current = setInterval(() => fetchLogs(false), 3000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [recording, fetchLogs]); const sortedLogs = useMemo(() => { const arr = [...logs]; const filtered = sourceFilter === "tests" ? arr.filter(isProviderTestLog) : sourceFilter === "traffic" ? arr.filter((l) => !isProviderTestLog(l)) : arr; filtered.sort((a, b) => { switch (sortBy) { case "oldest": return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); case "latency_desc": return (b.latencyMs || 0) - (a.latencyMs || 0); case "latency_asc": return (a.latencyMs || 0) - (b.latencyMs || 0); case "newest": default: return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); } }); return filtered; }, [logs, sortBy, sourceFilter]); const uniqueProviders = [...new Set(logs.map((l) => l.provider).filter(Boolean))].sort(); const uniqueTypes = [...new Set(logs.map((l) => l.proxy?.type).filter(Boolean))].sort(); const uniqueLevels = [...new Set(logs.map((l) => l.level).filter(Boolean))].sort(); const totalCount = logs.length; const okCount = logs.filter((l) => l.status === "success").length; const errorCount = logs.filter((l) => l.status === "error").length; const timeoutCount = logs.filter((l) => l.status === "timeout").length; const directCount = logs.filter((l) => l.level === "direct").length; const tlsCount = logs.filter((l) => l.tlsFingerprint).length; const testsCount = logs.filter(isProviderTestLog).length; return (