diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 9dc49f0..50ab372 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -41,6 +41,7 @@ import ToastContainer from './components/ToastContainer.jsx';
import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx';
import ErrorBoundary from './components/ErrorBoundary.jsx';
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
+import { PingProvider } from './contexts/PingContext.jsx';
// --- Simple i18n (RU/EN) ---
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
@@ -104,12 +105,14 @@ function App() {
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx
index d7147f4..faa2371 100644
--- a/frontend/src/EasySwitchManager.jsx
+++ b/frontend/src/EasySwitchManager.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import api from './lib/api.js';
+import { usePing } from './contexts/PingContext.jsx';
import {
IconSearch,
IconRefresh,
@@ -49,8 +50,8 @@ function EasySwitchManager() {
const [hasChanges, setHasChanges] = useState(false);
const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы
const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам
- // Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс
- const [pingMap, setPingMap] = useState({});
+ // Единый источник пингов (кеш + API) — общий с страницей фильтров
+ const { pingMap, requestPings } = usePing();
useEffect(() => {
const initData = async () => {
@@ -212,8 +213,21 @@ function EasySwitchManager() {
// Фильтруем серверы, у которых есть gateways
const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0);
setServers(serversWithGateways);
- // Предзагружаем пинги для всех комбинаций server+gateway
- prefetchPings(serversWithGateways, inventory);
+ // Единая точка входа: запрашиваем пинги через контекст (кеш общий с фильтрами)
+ const pingTasks = [];
+ serversWithGateways.forEach((server) => {
+ const inv = inventory.find(srv =>
+ String(srv.dns || '').trim() === server.name ||
+ String(srv.hostName || '').trim() === server.name ||
+ String(srv.ip || '').trim() === server.name
+ );
+ const routerId = inv?.id || inv?.dns || inv?.ip;
+ if (!routerId) return;
+ (server.gateways || []).forEach((gw) => {
+ if (gw.ip) pingTasks.push({ routerId, gatewayIp: gw.ip });
+ });
+ });
+ requestPings(pingTasks);
// Отладочная информация
try {
@@ -254,59 +268,6 @@ function EasySwitchManager() {
}
};
- /**
- * Загрузить реальные пинги для комбинаций (сервер, gateway).
- * Цель пинга берётся из настроек интерфейса («Домен для пинга») или по умолчанию.
- */
- const prefetchPings = async (serversList, inventory) => {
- try {
- const tasks = [];
- const seen = new Set();
-
- serversList.forEach((server) => {
- const inv = inventory.find(srv =>
- String(srv.dns || '').trim() === server.name ||
- String(srv.hostName || '').trim() === server.name ||
- String(srv.ip || '').trim() === server.name
- );
- const routerId = inv?.id || inv?.dns || inv?.ip;
- if (!routerId) return;
-
- (server.gateways || []).forEach((gw) => {
- const ip = gw.ip || '';
- if (!ip) return;
- const key = `${routerId}:${ip}`;
- if (seen.has(key) || pingMap[key] !== undefined) return;
- seen.add(key);
- tasks.push({ routerId, ip, key });
- });
- });
-
- if (tasks.length === 0) return;
-
- // Запросы идут параллельно; обновляем UI по мере прихода каждого ответа,
- // чтобы значения на интерфейсе отображались быстрее.
- tasks.forEach(({ routerId, ip, key }) => {
- api
- .post('/mikrotik/ping', {
- serverId: routerId,
- gatewayIp: ip,
- count: 5,
- })
- .then(({ data }) => {
- const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
- setPingMap((prev) => ({ ...prev, [key]: value }));
- })
- .catch((e) => {
- console.warn('Failed to fetch ping for', routerId, ip, e?.message || e);
- setPingMap((prev) => ({ ...prev, [key]: null }));
- });
- });
- } catch (e) {
- console.warn('prefetchPings error:', e?.message || e);
- }
- };
-
const handleGatewaySelect = (serverId, community, gateway) => {
const key = `${serverId}:${community}`;
setActiveGateways(prev => {
diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx
index 79a90d7..af981b0 100644
--- a/frontend/src/FilterManager.jsx
+++ b/frontend/src/FilterManager.jsx
@@ -51,6 +51,7 @@ import {
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { normalizeGateways, countryToFlag } from './utils/serverUtils.js';
+import { usePing } from './contexts/PingContext.jsx';
import {
AddFilterModal,
EditFilterModal,
@@ -782,8 +783,8 @@ function FilterManager() {
const [inventoryServers, setInventoryServers] = useState([]);
// Network config gateways (from /network-config endpoint)
const [networkConfigGateways, setNetworkConfigGateways] = useState([]);
- // Пинги по шлюзам: ключ `${routerId}:${gatewayIp}` → avg RTT в мс (как на Easy Switch)
- const [pingMap, setPingMap] = useState({});
+ // Единая точка входа для пингов (кеш общий с Easy Switch)
+ const { pingMap, requestPings } = usePing();
// Получить metadata сервера из inventory по имени (dns/hostName/ip)
const getInventoryMetaForServer = (serverName) => {
@@ -888,55 +889,17 @@ function FilterManager() {
});
})();
- // Предзагружаем пинги для gateways выбранного сервера (как на Easy Switch, но только для одного сервера)
- const prefetchPingsForSelectedServer = () => {
- try {
- if (!selectedServer) return;
- const routerId = getRouterIdForServer(selectedServer);
- if (!routerId) return;
- if (!selectedServerGateways || selectedServerGateways.length === 0) return;
-
- const tasks = [];
- const seen = new Set();
-
- selectedServerGateways.forEach((gw) => {
- const ip = gw.ip || '';
- if (!ip) return;
- const key = `${routerId}:${ip}`;
- if (seen.has(key) || Object.prototype.hasOwnProperty.call(pingMap, key)) return;
- seen.add(key);
- tasks.push({ routerId, ip, key });
- });
-
- if (tasks.length === 0) return;
-
- tasks.forEach(({ routerId: rId, ip, key }) => {
- api
- .post('/mikrotik/ping', {
- serverId: rId,
- gatewayIp: ip,
- count: 5,
- })
- .then(({ data }) => {
- const value =
- typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
- setPingMap((prev) => ({ ...prev, [key]: value }));
- })
- .catch((e) => {
- console.warn('Failed to fetch ping for', rId, ip, e?.message || e);
- setPingMap((prev) => ({ ...prev, [key]: null }));
- });
- });
- } catch (e) {
- console.warn('prefetchPingsForSelectedServer error:', e?.message || e);
- }
- };
-
- // Автоматически обновляем пинги при смене выбранного сервера или списка его gateways
+ // Запрашиваем пинги для gateways выбранного сервера через единый контекст (кеш общий с Easy Switch)
useEffect(() => {
- prefetchPingsForSelectedServer();
- }, [selectedServer, selectedServerGateways, inventoryServers]);
-
+ if (!selectedServer || !selectedServerGateways?.length) return;
+ const routerId = getRouterIdForServer(selectedServer);
+ if (!routerId) return;
+ const tasks = selectedServerGateways
+ .filter((gw) => gw.ip)
+ .map((gw) => ({ routerId, gatewayIp: gw.ip }));
+ requestPings(tasks);
+ }, [selectedServer, selectedServerGateways, requestPings]);
+
useEffect(() => {
// Загружаем базовую AS из настроек
(async () => {
diff --git a/frontend/src/contexts/PingContext.jsx b/frontend/src/contexts/PingContext.jsx
new file mode 100644
index 0000000..360364a
--- /dev/null
+++ b/frontend/src/contexts/PingContext.jsx
@@ -0,0 +1,92 @@
+import { createContext, useContext, useState, useCallback, useRef } from 'react';
+import api from '../lib/api.js';
+
+/**
+ * Единая точка входа для пингов до шлюзов (MikroTik).
+ * Кеш: ключ `${routerId}:${gatewayIp}` → number | null (мс или ошибка).
+ * Используется и на Easy Switch, и на странице фильтров (карточки gateway).
+ */
+const PingContext = createContext(null);
+
+const PING_COUNT = 5;
+
+export function PingProvider({ children }) {
+ const [pingMap, setPingMap] = useState({});
+ const inFlightRef = useRef(new Set());
+ const pingMapRef = useRef(pingMap);
+ pingMapRef.current = pingMap;
+
+ const getKey = (routerId, gatewayIp) => {
+ if (!routerId || !gatewayIp) return null;
+ return `${routerId}:${gatewayIp}`;
+ };
+
+ const getPing = useCallback((routerId, gatewayIp) => {
+ const key = getKey(routerId, gatewayIp);
+ if (!key) return undefined;
+ return Object.prototype.hasOwnProperty.call(pingMap, key) ? pingMap[key] : undefined;
+ }, [pingMap]);
+
+ /**
+ * Запросить пинги для списка пар (routerId, gatewayIp).
+ * Уже закешированные или запрашиваемые ключи пропускаются. Один вызов API на ключ.
+ * @param tasks Array<{ routerId, gatewayIp }>
+ */
+ const requestPings = useCallback((tasks) => {
+ if (!Array.isArray(tasks) || tasks.length === 0) return;
+ const cache = pingMapRef.current;
+
+ const toRequest = [];
+ tasks.forEach(({ routerId, gatewayIp }) => {
+ const ip = gatewayIp || '';
+ if (!routerId || !ip) return;
+ const key = getKey(routerId, ip);
+ if (!key) return;
+ if (Object.prototype.hasOwnProperty.call(cache, key)) return;
+ if (inFlightRef.current.has(key)) return;
+ inFlightRef.current.add(key);
+ toRequest.push({ routerId, ip, key });
+ });
+
+ toRequest.forEach(({ routerId, ip, key }) => {
+ api
+ .post('/mikrotik/ping', {
+ serverId: routerId,
+ gatewayIp: ip,
+ count: PING_COUNT,
+ })
+ .then(({ data }) => {
+ const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
+ setPingMap((prev) => ({ ...prev, [key]: value }));
+ })
+ .catch((e) => {
+ console.warn('PingContext: failed to fetch ping for', routerId, ip, e?.message || e);
+ setPingMap((prev) => ({ ...prev, [key]: null }));
+ })
+ .finally(() => {
+ inFlightRef.current.delete(key);
+ });
+ });
+ }, []);
+
+ const value = {
+ pingMap,
+ getPing,
+ requestPings,
+ getKey,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePing() {
+ const ctx = useContext(PingContext);
+ if (!ctx) {
+ throw new Error('usePing must be used within PingProvider');
+ }
+ return ctx;
+}