import { useCallback, useEffect, useMemo, useState } from 'react'; import api from './lib/api.js'; import { useNotify } from './components/NotifyProvider.jsx'; import PageHeader from './components/PageHeader.jsx'; import TableSkeleton from './components/TableSkeleton.jsx'; import { countryToFlag } from './utils/serverUtils.js'; import { IconTopologyRing, IconNetwork, IconSearch, IconRefresh, } from '@tabler/icons-react'; export default function InterfaceSpeedTest() { const notify = useNotify(); const [servers, setServers] = useState([]); const [networkConfig, setNetworkConfig] = useState(null); const [loading, setLoading] = useState(true); const [serverSearch, setServerSearch] = useState(''); const [serverId, setServerId] = useState(''); const [selectedInterfaceName, setSelectedInterfaceName] = useState(''); const [running, setRunning] = useState(false); const [speedResult, setSpeedResult] = useState(null); const [speedSettings, setSpeedSettings] = useState({ protocol: 'tcp', durationSeconds: 10, cacheMinutes: 0, }); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; const load = async () => { setLoading(true); try { const [serversRes, netRes, uiRes] = await Promise.all([ api.get('/servers', { signal }), api.get('/network-config', { signal }).catch(() => ({ data: null })), api.get('/ui-settings', { signal }).catch(() => ({ data: {} })), ]); setServers(Array.isArray(serversRes?.data) ? serversRes.data : []); setNetworkConfig( netRes?.data && typeof netRes.data === 'object' ? netRes.data : { gateways: [], tunnelInterfaces: [] } ); const ui = uiRes?.data || {}; const proto = String(ui.interfaceSpeedTestProtocol || 'tcp').toLowerCase() === 'udp' ? 'udp' : 'tcp'; const duration = Math.max( 1, Math.min( 600, parseInt(ui.interfaceSpeedTestDurationSeconds, 10) || 5 ) ); const cacheMinutes = Math.max( 0, parseInt(ui.interfaceSpeedTestCacheMinutes, 10) || 0 ); setSpeedSettings({ protocol: proto, durationSeconds: duration, cacheMinutes, }); } catch (e) { if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('[InterfaceSpeedTest] load failed', e); notify.error('Не удалось загрузить данные для замера скорости'); } finally { setLoading(false); } }; load(); return () => controller.abort(); }, [notify]); const _gateways = useMemo( () => networkConfig?.gateways && Array.isArray(networkConfig.gateways) ? networkConfig.gateways : [], [networkConfig] ); const tunnelInterfaces = useMemo( () => networkConfig?.tunnelInterfaces && Array.isArray(networkConfig.tunnelInterfaces) ? networkConfig.tunnelInterfaces : [], [networkConfig] ); const jumphostServers = useMemo( () => (servers || []).filter((s) => ['jumphost', 'home'].includes( String(s.type || '').toLowerCase() ) ), [servers] ); const makeServerId = (s) => (s.id || s.dns || s.ip || '').toString(); const jumphostInputServers = useMemo( () => jumphostServers.map((s) => ({ ...s, id: makeServerId(s) })), [jumphostServers] ); const visibleJumphostServers = useMemo(() => { if (!serverSearch.trim()) return jumphostInputServers; const q = serverSearch.trim().toLowerCase(); return jumphostInputServers.filter((s) => { const fields = [ s.id, s.ip, s.dns, s.extIp, s.internalIp, s.country, s.provider, ] .filter(Boolean) .join(' ') .toLowerCase(); return fields.includes(q); }); }, [jumphostInputServers, serverSearch]); const currentServer = useMemo( () => jumphostServers.find((s) => makeServerId(s) === serverId) || null, [jumphostServers, serverId] ); const interfacesForServer = useMemo(() => { if (!serverId || !tunnelInterfaces.length) return []; const ids = new Set( [serverId, currentServer?.id, currentServer?.ip, currentServer?.dns] .filter(Boolean) .map(String) ); return tunnelInterfaces.filter( (i) => i && i.name && (ids.has(String(i.serverId)) || ids.has(String(i.serverId2))) ); }, [tunnelInterfaces, serverId, currentServer]); const currentServerLabel = currentServer?.dns || currentServer?.ip || makeServerId(currentServer || {}) || 'Не выбран'; const isInitialLoading = loading && servers.length === 0; const formatMbps = (bps) => { if (bps == null || Number.isNaN(bps)) return '—'; const mbps = bps / 1_000_000; if (!Number.isFinite(mbps)) return '—'; return `${mbps.toFixed(2)} Мбит/с`; }; /** Запросить только кеш (без замера). При выборе сервера/интерфейса показываем кеш, если есть. */ const fetchCachedResult = useCallback(async () => { if (!serverId || !selectedInterfaceName) { setSpeedResult(null); return; } try { const res = await api.post('/mikrotik/speed-test', { serverId, interfaceName: selectedInterfaceName, cacheOnly: true, }); const data = res?.data || {}; if (data.ok !== false && !data.notInCache && (data.tcpDownloadBps != null || data.tcpUploadBps != null)) { setSpeedResult(data); } else { setSpeedResult(null); } } catch { setSpeedResult(null); } }, [serverId, selectedInterfaceName]); useEffect(() => { fetchCachedResult(); }, [fetchCachedResult]); const handleRunTest = async () => { if (!serverId) { notify.error('Выберите сервер (jumphost/home) для замера скорости'); return; } if (!selectedInterfaceName) { notify.error('Выберите туннельный интерфейс для замера скорости'); return; } setRunning(true); setSpeedResult(null); try { const body = { serverId, interfaceName: selectedInterfaceName, durationSeconds: speedSettings.durationSeconds, forceRefresh: true, }; const res = await api.post('/mikrotik/speed-test', body, { timeout: 120000, }); const data = res?.data || {}; if (data.ok === false) { notify.error( data.error || 'Замер скорости по интерфейсу завершился с ошибкой' ); return; } setSpeedResult(data); } catch (e) { console.error('[InterfaceSpeedTest] test failed', e); notify.error( e?.response?.data?.message || e?.message || 'Ошибка замера скорости' ); } finally { setRunning(false); } }; const handleReset = () => { setServerId(''); setSelectedInterfaceName(''); setSpeedResult(null); }; return (
} meta={`Текущий сервер: ${currentServerLabel}`} />

Серверы (jumphost / home)

Выберите роутер с настроенным MikroTik API

setServerSearch(e.target.value)} />
{isInitialLoading ? ( ) : jumphostServers.length === 0 ? (
Нет серверов типа jumphost или home. Добавьте их в разделе «Серверы», чтобы использовать замер скорости.
) : visibleJumphostServers.length === 0 ? (
По вашему запросу серверы не найдены. Попробуйте изменить фильтр поиска.
) : (
{visibleJumphostServers.map((s) => { const id = s.id; const isActive = id === serverId; const flag = s.country ? countryToFlag(s.country) : ''; const line = [s.dns, s.ip].filter(Boolean).join(' · '); return (
setServerId(id)} >
{flag || s.ip?.slice(0, 2) || '?'}
{s.dns || s.ip || id}
{line || [s.country, s.provider] .filter(Boolean) .join(' · ') || '—'}
); })}
)}
Сервер: {currentServerLabel}

Интерфейсы туннелей

{loading && !networkConfig ? (
Загрузка сетевых настроек…
) : !serverId ? (
Выберите сервер слева, затем интерфейс для замера скорости.
) : interfacesForServer.length === 0 ? (
Для выбранного сервера нет туннельных интерфейсов в /network-config. Добавьте интерфейсы в разделе «Сетевые настройки».
) : ( <>
Для сопоставления используется имя интерфейса из /network-config.tunnelInterfaces. В RouterOS имя должно совпадать.
)}

Параметры замера

{speedSettings.protocol === 'udp' ? 'UDP' : 'TCP'}
Значение берётся из настроек интерфейса.
{speedSettings.durationSeconds}
{speedSettings.cacheMinutes}

Скорость по интерфейсу MikroTik

{!speedResult ? (
Выберите сервер и интерфейс, затем запустите замер. Средние скорости приёма и передачи появятся здесь.
) : (
Интерфейс
{speedResult.interfaceName || selectedInterfaceName}
Замер за {speedResult.durationSeconds} сек {speedResult.cached ? ' (из кеша)' : ''}
TCP скорость
Download: {formatMbps(speedResult.tcpDownloadBps)}
Upload: {formatMbps(speedResult.tcpUploadBps)}
)}
); }