diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 4910d8a..41ccdc5 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -100,16 +100,23 @@ function ASNsNewManager() { const [wsUrl, setWsUrl] = useState(''); useEffect(() => { - // Load WS URL once + const controller = new AbortController(); (async () => { - try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} + try { + const r = await api.get('/ws/url', { signal: controller.signal }); + setWsUrl(String(r.data?.url || '')); + } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + } })(); + return () => controller.abort(); }, []); - const fetchItems = async () => { + const fetchItems = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } }); + const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const payload = Array.isArray(response.data?.items) ? response.data.items : []; const mapped = payload.map(item => ({ asn: String(item.domain), community: String(item.type) })); const total = mapped.length; @@ -122,6 +129,7 @@ function ASNsNewManager() { setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); setError(''); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching ASNs:', error); setError('Не удалось загрузить список ASN. Проверьте, запущен ли бэкенд.'); } finally { @@ -129,7 +137,14 @@ function ASNsNewManager() { } }; - useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []); + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + const controller = new AbortController(); + fetchItems(controller.signal); + return () => controller.abort(); + } + }, []); const handleAddItem = (item) => { const asn = item?.asn || newItem.asn; diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index 88fbf6e..52e5e4e 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -113,9 +113,12 @@ function BillingManager() { }); useEffect(() => { - fetchBillingData(); - fetchExchangeRates(); - fetchServers(); + const controller = new AbortController(); + const signal = controller.signal; + fetchBillingData(signal); + fetchExchangeRates(signal); + fetchServers(signal); + return () => controller.abort(); }, []); // Автоматическая синхронизация: добавляем серверы без записей в биллинге @@ -162,11 +165,12 @@ function BillingManager() { }, [servers, loading]); - const fetchExchangeRates = async () => { + const fetchExchangeRates = async (abortSignal) => { setRatesLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { // Основной источник: ЦБ РФ (base = RUB) - const cbr = await axios.get('https://www.cbr-xml-daily.ru/latest.js'); + const cbr = await axios.get('https://www.cbr-xml-daily.ru/latest.js', opts); const rubPerUsd = cbr.data && cbr.data.rates?.USD ? 1 / Number(cbr.data.rates.USD) : null; const rubPerEur = cbr.data && cbr.data.rates?.EUR ? 1 / Number(cbr.data.rates.EUR) : null; @@ -180,12 +184,13 @@ function BillingManager() { return; } } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.warn('ЦБ недоступен, пробуем резервный источник:', error?.message); } try { // Резервный источник: open.er-api - const response = await axios.get('https://open.er-api.com/v6/latest/USD'); + const response = await axios.get('https://open.er-api.com/v6/latest/USD', opts); const rates = response.data?.rates || {}; setExchangeRates({ USD: 1, @@ -193,6 +198,7 @@ function BillingManager() { RUB: rates.RUB || 1 }); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Ошибка при загрузке курсов валют:', error); // Фолбек на разумные значения setExchangeRates({ USD: 1, EUR: 1.05, RUB: 95 }); @@ -201,19 +207,22 @@ function BillingManager() { } }; - const fetchServers = async () => { + const fetchServers = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get(`/servers`); + const res = await api.get(`/servers`, opts); setServers(Array.isArray(res.data) ? res.data : []); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Ошибка при загрузке серверов:', error); } }; - const fetchBillingData = async () => { + const fetchBillingData = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/billing`); + const response = await api.get(`/billing`, opts); const rates = exchangeRates; // из closure, на первой загрузке может быть {1,1,1} — для USD достаточно const toRUB = (amt, ccy) => { if (!amt) return 0; @@ -251,6 +260,7 @@ function BillingManager() { setBillingData(normalized); setError(''); } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; console.error('Ошибка при загрузке данных биллинга:', err); setError('Ошибка при загрузке данных'); setBillingData([]); diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index ad029a8..a4cde87 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -202,18 +202,21 @@ function Dashboard() { const [expandedPingId, setExpandedPingId] = useState(null); useEffect(() => { + const controller = new AbortController(); + const signal = controller.signal; + async function fetchStats() { setLoading(true); setError(null); try { - // Загружаем данные с обработкой ошибок для каждого endpoint + const opts = { signal }; const results = await Promise.allSettled([ - api.get('/domains-new', { params: { countOnly: true } }), - api.get('/ip-ranges', { params: { countOnly: true } }), - api.get('/asns', { params: { countOnly: true } }), - api.get('/servers'), - api.get('/servers/availability', { params: { ttlSeconds: 60 } }), - api.get('/s3/last-modified') + api.get('/domains-new', { params: { countOnly: true }, ...opts }), + api.get('/ip-ranges', { params: { countOnly: true }, ...opts }), + api.get('/asns', { params: { countOnly: true }, ...opts }), + api.get('/servers', opts), + api.get('/servers/availability', { params: { ttlSeconds: 60 }, ...opts }), + api.get('/s3/last-modified', opts) ]); // Обрабатываем результаты @@ -278,6 +281,7 @@ function Dashboard() { console.warn('Некоторые API недоступны:', errors.map(e => e.reason?.message)); } } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('Критическая ошибка загрузки статистики:', e); setError('Не удалось загрузить статистику'); setStats({ @@ -296,23 +300,26 @@ function Dashboard() { } } fetchStats(); + return () => controller.abort(); }, []); useEffect(() => { - let cancelled = false; - api.get('/ping-services-list') + const controller = new AbortController(); + api.get('/ping-services-list', { signal: controller.signal }) .then(({ data }) => { - if (!cancelled && Array.isArray(data?.list)) setPingServicesConfig(data.list); + if (Array.isArray(data?.list)) setPingServicesConfig(data.list); }) - .catch(() => { - if (!cancelled) setPingServicesConfig([]); + .catch((e) => { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + setPingServicesConfig([]); }); - return () => { cancelled = true; }; + return () => controller.abort(); }, []); - function fetchPingServices(forceRefresh = false) { + function fetchPingServices(forceRefresh = false, abortSignal) { setPingLoading(true); - api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {} }) + const opts = abortSignal ? { signal: abortSignal } : {}; + api.get('/ping-services', { params: forceRefresh ? { refresh: 1 } : {}, ...opts }) .then(({ data }) => { if (!data || typeof data !== 'object') return; const byId = data.byId ?? data; @@ -320,12 +327,17 @@ function Dashboard() { setPingServices(byId); setPingHistory(typeof history === 'object' ? history : {}); }) - .catch(() => setPingServices(null)) + .catch((e) => { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + setPingServices(null); + }) .finally(() => setPingLoading(false)); } useEffect(() => { - fetchPingServices(); + const controller = new AbortController(); + fetchPingServices(false, controller.signal); + return () => controller.abort(); }, []); if (error) { diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index f50c181..19e78d3 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -86,14 +86,25 @@ function DomainsNewManager() { // Справочник community для подсказок const [communities, setCommunities] = useState([]); useEffect(() => { + const controller = new AbortController(); + const signal = controller.signal; (async () => { try { - const res = await api.get(`/communities`); + const res = await api.get(`/communities`, { signal }); setCommunities(Array.isArray(res.data) ? res.data : []); } catch (e) { - // тихо игнорируем + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; } })(); + (async () => { + try { + const r = await api.get('/ws/url', { signal }); + setWsUrl(String(r.data?.url || '')); + } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + } + })(); + return () => controller.abort(); }, []); const renderCommunityBadge = (value) => (); @@ -110,14 +121,11 @@ function DomainsNewManager() { const [wsOpen, setWsOpen] = useState(false); const [wsUrl, setWsUrl] = useState(''); - useEffect(() => { - (async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })(); - }, []); - - const fetchItems = async () => { + const fetchItems = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } }); + const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const payload = Array.isArray(response.data?.items) ? response.data.items : []; const total = payload.length; setItems(payload); @@ -129,6 +137,7 @@ function DomainsNewManager() { setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); setError(''); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching domains-new:', error); setError('Не удалось загрузить домены. Проверьте, запущен ли бэкенд.'); } finally { @@ -136,8 +145,14 @@ function DomainsNewManager() { } }; - // Подгрузка при изменении страницы/поиска/фильтра - useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []); + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + const controller = new AbortController(); + fetchItems(controller.signal); + return () => controller.abort(); + } + }, []); const handleAddItem = (item) => { // Если item передан (из модалки), используем его, иначе из state (старый способ) diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index 558cfd3..6ebb9a6 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -57,39 +57,46 @@ function EasySwitchManager() { const location = useLocation(); useEffect(() => { + const controller = new AbortController(); + const signal = controller.signal; + const initData = async () => { try { // Загружаем communities - const commRes = await api.get(`/communities`); + const commRes = await api.get(`/communities`, { signal }); const communities = Array.isArray(commRes.data) ? commRes.data : []; setCommunitiesDirectory(communities); - + // Загружаем inventory servers - const invRes = await api.get(`/servers`); + const invRes = await api.get(`/servers`, { signal }); const inventory = Array.isArray(invRes.data) ? invRes.data : []; setInventoryServers(inventory); // Загружаем network-config (gateways и туннели) let ncGateways = []; try { - const ncRes = await api.get(`/network-config`); + const ncRes = await api.get(`/network-config`, { signal }); const ncData = ncRes.data || {}; ncGateways = Array.isArray(ncData.gateways) ? ncData.gateways : []; } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; // Если файла нет или ошибка - используем пустой массив (fallback на старую логику) console.warn('EasySwitch: не удалось загрузить /network-config, используется fallback:', e?.message || e); ncGateways = []; } setNetworkConfigGateways(ncGateways); - + // Загружаем данные серверов и фильтров - await loadDataWithParams(communities, inventory, ncGateways); + await loadDataWithParams(communities, inventory, ncGateways, signal); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('Error initializing data:', e); setError('Ошибка загрузки данных'); } }; initData(); + + return () => controller.abort(); }, []); // При переходе на раздел Easy Switch перезапрашиваем пинги (устаревшие по TTL обновятся) @@ -111,21 +118,23 @@ function EasySwitchManager() { requestPings(pingTasks); }, [location.pathname, servers, inventoryServers, requestPings]); - const loadDataWithParams = async (communities, inventory, ncGatewaysParam) => { + const loadDataWithParams = async (communities, inventory, ncGatewaysParam, signal) => { setLoading(true); setError(''); - + + const requestConfig = signal ? { signal } : {}; + try { // Загружаем список серверов из server-configs - const serversResponse = await api.get(`/server-configs`); + const serversResponse = await api.get(`/server-configs`, requestConfig); const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : []; - + // Загружаем фильтры для каждого сервера (для определения активных gateways) const serversWithData = await Promise.all( serversList.map(async (server) => { try { // Загружаем filters для сервера - const filtersResponse = await api.get(`/server-filters/${server.id}`); + const filtersResponse = await api.get(`/server-filters/${server.id}`, requestConfig); const filters = Array.isArray(filtersResponse.data) ? filtersResponse.data : []; // Получаем gateways сервера: @@ -283,6 +292,7 @@ function EasySwitchManager() { } catch (_) {} } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; console.error('Error loading data:', err); setError('Ошибка загрузки данных. Проверьте подключение к серверу.'); } finally { diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index e628969..679bed9 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -896,47 +896,51 @@ function FilterManager() { }, [location.pathname, selectedServer, selectedServerGateways, requestPings]); useEffect(() => { - // Загружаем базовую AS из настроек + const controller = new AbortController(); + const signal = controller.signal; + const opts = { signal }; + (async () => { try { - const res = await api.get('/ui-settings'); + const res = await api.get('/ui-settings', opts); const data = res?.data || {}; const loadedBaseAS = String(data?.baseAS || '65001'); setBaseAS(loadedBaseAS); } catch (e) { - // мягко игнорируем, используем значение по умолчанию + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; } })(); - + (async () => { try { - const res = await api.get(`/communities`); + const res = await api.get(`/communities`, opts); setCommunities(Array.isArray(res.data) ? res.data : []); } catch (e) { - // мягко игнорируем, автокомплит необязателен + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; } })(); - // Загружаем инвентарь серверов для отображения флагов и IP + (async () => { try { - const res = await api.get(`/servers`); + const res = await api.get(`/servers`, opts); setInventoryServers(Array.isArray(res.data) ? res.data : []); } catch (e) { - // мягко игнорируем, дополнительная информация необязательна + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; } })(); - - // Загружаем gateways из NetworkConfigManager + (async () => { try { - const res = await api.get(`/network-config`); + const res = await api.get(`/network-config`, opts); const networkConfig = res.data || {}; setNetworkConfigGateways(Array.isArray(networkConfig.gateways) ? networkConfig.gateways : []); } catch (e) { - // Если файла нет или ошибка - используем пустой массив (fallback на старую логику) + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setNetworkConfigGateways([]); } })(); + + return () => controller.abort(); }, []); const filterKey = (f) => { @@ -945,7 +949,9 @@ function FilterManager() { }; useEffect(() => { - fetchServers(); + const controller = new AbortController(); + fetchServers(controller.signal); + return () => controller.abort(); }, []); // Загружаем фильтры при выборе сервера @@ -1003,11 +1009,12 @@ function FilterManager() { // Функции для управления серверами // Загружаем jumphost'ы из раздела "Серверы" (/api/servers) // + загружаем старые server-configs для маппинга legacyId - const fetchServers = async () => { + const fetchServers = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { const [serversRes, legacyRes] = await Promise.all([ - api.get(`/servers`), - api.get(`/server-configs`).catch(() => ({ data: [] })) // Старые конфиги для маппинга + api.get(`/servers`, opts), + api.get(`/server-configs`, opts).catch(() => ({ data: [] })) // Старые конфиги для маппинга ]); const allServers = Array.isArray(serversRes.data) ? serversRes.data : []; @@ -1064,6 +1071,7 @@ function FilterManager() { setServers(jumphosts); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching servers:', error); setServers([]); } diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 8c7727e..5701671 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -114,13 +114,23 @@ function IPRangesManager() { const [wsUrl, setWsUrl] = useState(''); useEffect(() => { - (async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })(); + const controller = new AbortController(); + (async () => { + try { + const r = await api.get('/ws/url', { signal: controller.signal }); + setWsUrl(String(r.data?.url || '')); + } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + } + })(); + return () => controller.abort(); }, []); - const fetchItems = async () => { + const fetchItems = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' } }); + const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' }, ...opts }); const payload = Array.isArray(response.data?.items) ? response.data.items : []; const total = payload.length; setItems(payload); @@ -132,6 +142,7 @@ function IPRangesManager() { setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null); setError(''); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching ip-ranges:', error); setError('Не удалось загрузить IP-диапазоны. Проверьте, запущен ли бэкенд.'); } finally { @@ -139,7 +150,14 @@ function IPRangesManager() { } }; - useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []); + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + const controller = new AbortController(); + fetchItems(controller.signal); + return () => controller.abort(); + } + }, []); const handleAddItem = (item) => { const ipRange = item?.ipRange || newItem.ipRange; diff --git a/frontend/src/InterfaceSpeedTest.jsx b/frontend/src/InterfaceSpeedTest.jsx index a3a2aea..6cee9cf 100644 --- a/frontend/src/InterfaceSpeedTest.jsx +++ b/frontend/src/InterfaceSpeedTest.jsx @@ -30,13 +30,16 @@ export default function InterfaceSpeedTest() { }); 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'), - api.get('/network-config').catch(() => ({ data: null })), - api.get('/ui-settings').catch(() => ({ data: {} })), + 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 : []); @@ -69,6 +72,7 @@ export default function InterfaceSpeedTest() { cacheMinutes, }); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('[InterfaceSpeedTest] load failed', e); notify.error('Не удалось загрузить данные для замера скорости'); } finally { @@ -76,6 +80,7 @@ export default function InterfaceSpeedTest() { } }; load(); + return () => controller.abort(); }, [notify]); const gateways = useMemo( diff --git a/frontend/src/MikrotikBackupsManager.jsx b/frontend/src/MikrotikBackupsManager.jsx index 14f1ff9..44b666b 100644 --- a/frontend/src/MikrotikBackupsManager.jsx +++ b/frontend/src/MikrotikBackupsManager.jsx @@ -157,13 +157,18 @@ function MikrotikBackupsManager() { const backupPageSize = 10; useEffect(() => { - fetchServers(); - fetchUiSettings(); + const controller = new AbortController(); + const signal = controller.signal; + fetchServers(signal); + fetchUiSettings(signal); + return () => controller.abort(); }, []); useEffect(() => { if (selectedServerId) { - fetchBackups(selectedServerId); + const controller = new AbortController(); + fetchBackups(selectedServerId, controller.signal); + return () => controller.abort(); } else { setBackups([]); setSelectedKeys([]); @@ -314,10 +319,11 @@ function MikrotikBackupsManager() { setBackupServerIds(new Set()); }; - const fetchServers = async () => { + const fetchServers = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { setLoadingServers(true); - const res = await api.get('/servers'); + const res = await api.get('/servers', opts); const list = Array.isArray(res.data) ? res.data : []; setServers(list); const firstJumphost = list.find((s) => String(s.type || '').toLowerCase() === 'jumphost'); @@ -325,6 +331,7 @@ function MikrotikBackupsManager() { setSelectedServerId(makeServerId(firstJumphost)); } } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; console.error('Error fetching servers for backups:', err); notify.error('Не удалось загрузить список серверов для бэкапов'); } finally { @@ -332,9 +339,10 @@ function MikrotikBackupsManager() { } }; - const fetchUiSettings = async () => { + const fetchUiSettings = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get('/ui-settings'); + const res = await api.get('/ui-settings', opts); const data = res?.data || {}; setUiSettings(data); const list = Array.isArray(data.mikrotikBackupServers) ? data.mikrotikBackupServers : []; @@ -342,21 +350,23 @@ function MikrotikBackupsManager() { const e = res?.headers?.etag || res?.headers?.ETag || ''; setUiSettingsEtag(e ? String(e) : ''); } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; console.error('Error fetching UI settings for backups:', err); - // мягко игнорируем, просто будут дефолты } }; - const fetchBackups = async (serverId) => { + const fetchBackups = async (serverId, abortSignal) => { if (!serverId) return; + const opts = abortSignal ? { signal: abortSignal } : {}; try { setLoadingBackups(true); setSelectedKeys([]); setDiffResult(null); - const res = await api.get('/mikrotik/backups', { params: { serverId } }); + const res = await api.get('/mikrotik/backups', { params: { serverId }, ...opts }); const items = res.data?.items || []; setBackups(items); } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; console.error('Error fetching backups:', err); notify.error('Не удалось загрузить список бэкапов MikroTik'); } finally { diff --git a/frontend/src/NetworkMapDashboard.jsx b/frontend/src/NetworkMapDashboard.jsx index 9b5c0fc..1b06533 100644 --- a/frontend/src/NetworkMapDashboard.jsx +++ b/frontend/src/NetworkMapDashboard.jsx @@ -86,16 +86,17 @@ export default function NetworkMapDashboard() { const cacheAppliedAtRef = useRef(null); /** Связи строятся из интерфейсов раздела «Сетевые настройки» (/network-config), tunnelInterfaces */ - const fetchData = useCallback(async () => { + const fetchData = useCallback(async (abortSignal) => { setLoading(true); setError(null); cacheAppliedAtRef.current = null; + const opts = abortSignal ? { signal: abortSignal } : {}; try { const [serversRes, configRes, uiSettingsRes, cacheRes] = await Promise.all([ - api.get('/servers'), - api.get('/network-config').catch(() => ({ data: null })), - api.get('/ui-settings').catch(() => ({ data: {} })), - api.get('/network-map-cache').catch(() => ({ data: {} })), + api.get('/servers', opts), + api.get('/network-config', opts).catch(() => ({ data: null })), + api.get('/ui-settings', opts).catch(() => ({ data: {} })), + api.get('/network-map-cache', opts).catch(() => ({ data: {} })), ]); const serversList = Array.isArray(serversRes?.data) ? serversRes.data : []; const config = configRes?.data || {}; @@ -181,6 +182,7 @@ export default function NetworkMapDashboard() { setSpeedStaleMap({}); } } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('NetworkMap fetch:', e); setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить данные'); setServers([]); @@ -342,7 +344,9 @@ export default function NetworkMapDashboard() { }, [connections]); useEffect(() => { - fetchData(); + const controller = new AbortController(); + fetchData(controller.signal); + return () => controller.abort(); }, [fetchData]); // Пинг и скорость не запускаются автоматически при открытии карты (даже если кеш просрочен). diff --git a/frontend/src/NetworkMapSchedulerPage.jsx b/frontend/src/NetworkMapSchedulerPage.jsx index 9981f9c..494b8a9 100644 --- a/frontend/src/NetworkMapSchedulerPage.jsx +++ b/frontend/src/NetworkMapSchedulerPage.jsx @@ -33,9 +33,10 @@ export default function NetworkMapSchedulerPage() { const logContainerRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); - const fetchSettings = useCallback(async () => { + const fetchSettings = useCallback(async (abortSignal) => { try { - const { data } = await api.get('/scheduler/network-map/settings'); + const opts = abortSignal ? { signal: abortSignal } : {}; + const { data } = await api.get('/scheduler/network-map/settings', opts); setSettings({ enabled: Boolean(data.enabled), intervalMinutes: Math.max(INTERVAL_MIN, Math.min(INTERVAL_MAX, Number(data.intervalMinutes) || 5)), @@ -45,6 +46,7 @@ export default function NetworkMapSchedulerPage() { }); setNextRunAt(typeof data.nextRunAt === 'number' ? data.nextRunAt : null); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить настройки'); } }, []); @@ -59,13 +61,13 @@ export default function NetworkMapSchedulerPage() { }, []); useEffect(() => { - let cancelled = false; + const controller = new AbortController(); (async () => { setLoading(true); - await fetchSettings(); - if (!cancelled) setLoading(false); + await fetchSettings(controller.signal); + setLoading(false); })(); - return () => { cancelled = true; }; + return () => controller.abort(); }, [fetchSettings]); useEffect(() => { diff --git a/frontend/src/PingServicesManager.jsx b/frontend/src/PingServicesManager.jsx index 10961cd..218dd98 100644 --- a/frontend/src/PingServicesManager.jsx +++ b/frontend/src/PingServicesManager.jsx @@ -58,11 +58,12 @@ export default function PingServicesManager() { const [editingItem, setEditingItem] = useState(null); const [editDraft, setEditDraft] = useState({ id: '', name: '', host: '', port: '443', icon: 'IconBrandGoogle', color: 'blue' }); - const loadSettings = async () => { + const loadSettings = async (abortSignal) => { setLoading(true); setError(''); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get('/ui-settings'); + const res = await api.get('/ui-settings', opts); const data = res?.data || {}; setRawSettings(data); setEtag(String(res?.headers?.etag || res?.headers?.ETag || '')); @@ -76,6 +77,7 @@ export default function PingServicesManager() { color: typeof s?.color === 'string' ? s.color : 'primary', })).filter((s) => s.host)); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error(e); setError('Не удалось загрузить настройки'); } finally { @@ -84,7 +86,9 @@ export default function PingServicesManager() { }; useEffect(() => { - loadSettings(); + const controller = new AbortController(); + loadSettings(controller.signal); + return () => controller.abort(); }, []); const saveSettings = async () => { diff --git a/frontend/src/ResourceStatsPage.jsx b/frontend/src/ResourceStatsPage.jsx index e5f0524..b2ff8d0 100644 --- a/frontend/src/ResourceStatsPage.jsx +++ b/frontend/src/ResourceStatsPage.jsx @@ -146,14 +146,16 @@ export default function ResourceStatsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const fetchStats = useCallback(async () => { + const fetchStats = useCallback(async (abortSignal) => { setLoading(true); setError(null); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get('/resources/stats'); + const res = await api.get('/resources/stats', opts); const data = res?.data; setRouters(Array.isArray(data?.routers) ? data.routers : []); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику'); setRouters([]); } finally { @@ -162,7 +164,9 @@ export default function ResourceStatsPage() { }, []); useEffect(() => { - fetchStats(); + const controller = new AbortController(); + fetchStats(controller.signal); + return () => controller.abort(); }, [fetchStats]); return ( diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 3bc6cb4..02dcce0 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -127,21 +127,26 @@ function ServerManager() { }; // === Загрузка данных === - const fetchNetworkConfig = async () => { + const fetchNetworkConfig = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get('/network-config'); + const res = await api.get('/network-config', opts); const config = res.data || {}; setNetworkConfigGateways(Array.isArray(config.gateways) ? config.gateways : []); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.warn('Network config not loaded:', e?.message || e); setNetworkConfigGateways([]); } }; useEffect(() => { - fetchServers(); - fetchConnections(); - fetchNetworkConfig(); + const controller = new AbortController(); + const signal = controller.signal; + fetchServers(signal); + fetchConnections(signal); + fetchNetworkConfig(signal); + return () => controller.abort(); }, []); useEffect(() => { @@ -156,10 +161,11 @@ function ServerManager() { } }, []); - const fetchServers = async () => { + const fetchServers = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/servers`); + const response = await api.get(`/servers`, opts); const normalized = (Array.isArray(response.data) ? response.data : []).map((srv) => ({ ...srv, type: normalizeServerType(srv.type), @@ -167,6 +173,7 @@ function ServerManager() { })); setServers(normalized); } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching servers:', error); notify.error('Не удалось загрузить серверы. Проверьте, запущен ли бэкенд.'); } finally { @@ -174,15 +181,17 @@ function ServerManager() { } }; - const fetchConnections = async () => { + const fetchConnections = async (abortSignal) => { + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const response = await api.get(`/server-connections`); + const response = await api.get(`/server-connections`, opts); if (Array.isArray(response.data)) { setConnections(response.data); } else { setConnections([]); } } catch (error) { + if (error?.name === 'CanceledError' || error?.name === 'AbortError' || error?.code === 'ERR_CANCELED') return; console.error('Error fetching server connections:', error); } }; diff --git a/frontend/src/TrafficDashboard.jsx b/frontend/src/TrafficDashboard.jsx index d33dd23..114f60d 100644 --- a/frontend/src/TrafficDashboard.jsx +++ b/frontend/src/TrafficDashboard.jsx @@ -181,18 +181,20 @@ export default function TrafficDashboard() { const interfaceFilter = getTrafficInterfaceFilter(uiSettings); - const fetchStats = useCallback(async () => { + const fetchStats = useCallback(async (abortSignal) => { setLoading(true); setError(null); + const opts = abortSignal ? { signal: abortSignal } : {}; try { const [statsRes, settingsRes] = await Promise.all([ - api.get('/traffic/interface-stats'), - api.get('/ui-settings').catch(() => ({ data: {} })), + api.get('/traffic/interface-stats', opts), + api.get('/ui-settings', opts).catch(() => ({ data: {} })), ]); const data = statsRes?.data || {}; setJumphosts(Array.isArray(data?.jumphosts) ? data.jumphosts : []); setUiSettings(settingsRes?.data || {}); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; console.error('Traffic stats:', e); setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику'); setJumphosts([]); @@ -202,7 +204,9 @@ export default function TrafficDashboard() { }, []); useEffect(() => { - fetchStats(); + const controller = new AbortController(); + fetchStats(controller.signal); + return () => controller.abort(); }, [fetchStats]); const filteredJumphosts = useMemo(() => { diff --git a/frontend/src/UptimeMonitorPage.jsx b/frontend/src/UptimeMonitorPage.jsx index cf3da16..4268243 100644 --- a/frontend/src/UptimeMonitorPage.jsx +++ b/frontend/src/UptimeMonitorPage.jsx @@ -103,13 +103,16 @@ export default function UptimeMonitorPage() { [servers] ); - const fetchServers = useCallback(async () => { + const controllerRef = useRef(null); + + const fetchServers = useCallback(async (abortSignal) => { setLoading(true); setError(null); + const opts = abortSignal ? { signal: abortSignal } : {}; try { const [serversRes, settingsRes] = await Promise.all([ - api.get('/servers'), - api.get('/ui-settings').catch(() => ({ data: {} })), + api.get('/servers', opts), + api.get('/ui-settings', opts).catch(() => ({ data: {} })), ]); const data = serversRes?.data; setServers(Array.isArray(data) ? data : []); @@ -117,6 +120,7 @@ export default function UptimeMonitorPage() { const interval = Math.max(30, Math.min(86400, parseInt(ui.uptimeMonitorIntervalSeconds, 10) || DEFAULT_CHECK_INTERVAL_SEC)); setCheckIntervalSeconds(interval); } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить серверы'); setServers([]); } finally { @@ -125,19 +129,29 @@ export default function UptimeMonitorPage() { }, []); useEffect(() => { - fetchServers(); + const c = new AbortController(); + controllerRef.current = c; + fetchServers(c.signal); + return () => { + c.abort(); + controllerRef.current = null; + }; }, [fetchServers]); // При заходе на страницу подставляем кеш — сразу видны последние проверки useEffect(() => { if (jumphosts.length === 0) return; - api.get('/uptime/cache') + const signal = controllerRef.current?.signal; + if (!signal) return; + api.get('/uptime/cache', { signal }) .then(({ data }) => { if (data?.results && typeof data.results === 'object' && Object.keys(data.results).length > 0) { setStatusMap((prev) => ({ ...prev, ...data.results })); } }) - .catch(() => {}); + .catch((e) => { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + }); }, [jumphosts]); const checkIntervalMs = checkIntervalSeconds * 1000; diff --git a/frontend/src/components/S3MetaBar.jsx b/frontend/src/components/S3MetaBar.jsx index de263b7..adc8e4c 100644 --- a/frontend/src/components/S3MetaBar.jsx +++ b/frontend/src/components/S3MetaBar.jsx @@ -7,19 +7,25 @@ function S3MetaBar({ keys = [], className = '' }) { const [meta, setMeta] = useState({}); const [loading, setLoading] = useState(false); - const fetchMeta = async () => { + const fetchMeta = async (abortSignal) => { setLoading(true); + const opts = abortSignal ? { signal: abortSignal } : {}; try { - const res = await api.get('/s3/last-modified'); + const res = await api.get('/s3/last-modified', opts); setMeta(res.data || {}); - } catch { + } catch (e) { + if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setMeta({}); } finally { setLoading(false); } }; - useEffect(() => { fetchMeta(); }, []); + useEffect(() => { + const controller = new AbortController(); + fetchMeta(controller.signal); + return () => controller.abort(); + }, []); const copyMeta = async (k) => { try {