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 (
Выберите роутер с настроенным MikroTik API
jumphost или home.
Добавьте их в разделе «Серверы», чтобы использовать замер
скорости.
/network-config. Добавьте интерфейсы в разделе
«Сетевые настройки».
/network-config.tunnelInterfaces. В RouterOS
имя должно совпадать.