"use client"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useTranslations } from "next-intl"; import Card from "./Card"; import ProxyLogDetail from "./ProxyLogDetail"; import { TYPE_COLORS, LEVEL_COLORS, PROVIDER_COLORS, getProxyStatusStyle as getStatusStyle, } from "@/shared/constants/colors"; import { formatTime, formatDuration as formatLatency, truncateUrl, } from "@/shared/utils/formatting"; import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; 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"; const PROXY_COLUMN_KEYS = [ "status", "proxy", "tls", "type", "level", "provider", "target", "latency", "ip", "time", ]; const DEFAULT_VISIBLE = Object.fromEntries(PROXY_COLUMN_KEYS.map((key) => [key, true])); export default function ProxyLogger() { const t = useTranslations("proxyLogger"); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [recording, setRecording] = useState(true); const [search, setSearch] = useState(""); const [activeFilter, setActiveFilter] = 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 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: "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]; arr.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 arr; }, [logs, sortBy]); 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; 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" />
{/* Type Dropdown */} {/* Level Dropdown */} {/* Provider Dropdown */} {/* Stats */}
{totalCount} {t("total")} {okCount} {t("ok")} {errorCount > 0 && ( {errorCount} {t("err")} )} {timeoutCount > 0 && ( {timeoutCount} {t("timeoutShort")} )} {directCount > 0 && ( {directCount} {t("direct")} )} {tlsCount > 0 && ( 🔒 {tlsCount} TLS )}
{/* Sort */} {/* Refresh */}
{/* Quick Filters */}
{statusFilters.map((f) => ( ))} {uniqueProviders.length > 0 && } {uniqueProviders.map((p) => { const pc = PROVIDER_COLORS[p] || { bg: "#374151", text: "#fff", label: p.toUpperCase() }; const isActive = selectedProvider === p; return ( ); })}
{/* Column Visibility Toggles */}
{t("columns")} {columns.map((col) => ( ))}
{/* Table */}
{loading && logs.length === 0 ? (
{t("loadingProxyLogs")}
) : logs.length === 0 ? (
vpn_lock {t("noProxyLogs")}
) : sortedLogs.length === 0 ? (
{t("noMatchingLogs")}
) : ( {visibleColumns.status && ( )} {visibleColumns.proxy && ( )} {visibleColumns.tls && ( )} {visibleColumns.type && ( )} {visibleColumns.level && ( )} {visibleColumns.provider && ( )} {visibleColumns.target && ( )} {visibleColumns.latency && ( )} {visibleColumns.ip && ( )} {visibleColumns.time && ( )} {sortedLogs.map((log) => { const statusStyle = getStatusStyle(log.status); const typeColor = TYPE_COLORS[log.proxy?.type] || { bg: "#6B7280", text: "#fff", label: log.proxy?.type || "-", }; const levelColor = LEVEL_COLORS[log.level] || LEVEL_COLORS.direct; const resolvedProviderLabel = getProviderDisplayLabel(log.provider, providerNodes) || (log.provider || "-").toUpperCase(); const providerColor = PROVIDER_COLORS[log.provider] || { bg: "#374151", text: "#fff", label: resolvedProviderLabel, }; const isError = log.status === "error" || log.status === "timeout"; return ( setSelectedLog(selectedLog?.id === log.id ? null : log)} className={`cursor-pointer hover:bg-sky-500/10 dark:hover:bg-sky-400/10 transition-colors ${isError ? "bg-red-500/5" : ""}`} > {visibleColumns.status && ( )} {visibleColumns.proxy && ( )} {visibleColumns.tls && ( )} {visibleColumns.type && ( )} {visibleColumns.level && ( )} {visibleColumns.provider && ( )} {visibleColumns.target && ( )} {visibleColumns.latency && ( )} {visibleColumns.ip && ( )} {visibleColumns.time && ( )} ); })}
{t("colStatus")}{t("colProxy")}{t("colTls")}{t("colType")}{t("colLevel")}{t("colProvider")}{t("colTarget")}{t("colLatency")}{t("colClientIp")}{t("colTime")}
{log.status} {log.proxy ? `${log.proxy.host}:${log.proxy.port}` : "—"} {log.tlsFingerprint ? ( 🔒 TLS ) : ( — )} {typeColor.label} {levelColor.label} {log.provider ? ( {providerColor.label} ) : ( — )} {truncateUrl(log.targetUrl)} {formatLatency(log.latencyMs)} {log.clientIp || "—"} {formatTime(log.timestamp)}
)}
{/* Detail Panel */} {selectedLog && setSelectedLog(null)} />}
); }