import React, { useEffect, useMemo, useRef, useState } from 'react'; import { formatTime } from '../utils/format.js'; const MAX_ENTRIES = 800; const MAX_TRAFFIC = 500; const GROUP_WINDOW_MS = 30_000; function normalizeLine(line) { return String(line || '').replace(/\x1b\[\d+m/g, '').trim(); } function groupEntries(entries) { const out = []; for (const e of entries) { const key = `${e.level}|${normalizeLine(e.line)}`; const last = out[out.length - 1]; const ts = new Date(e.ts).getTime(); if (last && last._key === key && ts - last._lastTs < GROUP_WINDOW_MS) { last.count += 1; last._lastTs = ts; last.lastTs = e.ts; } else { out.push({ ...e, _key: key, _lastTs: ts, count: 1, lastTs: e.ts }); } } return out; } const CATEGORY_BADGE = { direct: { cls: 'success', label: 'direct' }, vpn: { cls: 'info', label: 'VPN' }, block: { cls: 'danger', label: 'block' }, other: { cls: '', label: 'other' }, }; function TrafficTab() { const [traffic, setTraffic] = useState([]); const [paused, setPaused] = useState(false); const [filter, setFilter] = useState('all'); // all | direct | vpn | block const [search, setSearch] = useState(''); const [autoscroll, setAutoscroll] = useState(true); const containerRef = useRef(null); const pausedRef = useRef(false); useEffect(() => { pausedRef.current = paused; }, [paused]); useEffect(() => { const source = new EventSource('/api/traffic/stream'); source.onmessage = (ev) => { if (pausedRef.current) return; try { const entry = JSON.parse(ev.data); setTraffic((prev) => { const next = [...prev, entry]; if (next.length > MAX_TRAFFIC) next.splice(0, next.length - MAX_TRAFFIC); return next; }); } catch {} }; return () => source.close(); }, []); const filtered = useMemo(() => { let list = traffic; if (filter !== 'all') list = list.filter((e) => e.category === filter); if (search) { const s = search.toLowerCase(); list = list.filter((e) => e.host?.toLowerCase().includes(s) || String(e.port || '').includes(s) || e.outbound?.toLowerCase().includes(s) || e.matchedRule?.toLowerCase().includes(s), ); } return list; }, [traffic, filter, search]); useEffect(() => { if (!autoscroll || !containerRef.current) return; containerRef.current.scrollTop = containerRef.current.scrollHeight; }, [filtered, autoscroll]); const counts = useMemo(() => { const c = { direct: 0, vpn: 0, block: 0 }; for (const e of traffic) if (e.category in c) c[e.category]++; return c; }, [traffic]); return (
| Время | Туннель | Хост / IP | Порт | Правило |
|---|---|---|---|---|
| {formatTime(e.ts)} | {badge.label} | {e.host || '—'} | {e.port || '—'} | {e.matchedRule ? {e.matchedRule} : —} |
Логов пока нет.
} {filtered.map((entry, index) => { const text = normalizeLine(entry.line); if (grouped && entry.count > 1) { return (