diff --git a/frontend/src/NetworkMapDashboard.jsx b/frontend/src/NetworkMapDashboard.jsx index d52a3bf..6d4974a 100644 --- a/frontend/src/NetworkMapDashboard.jsx +++ b/frontend/src/NetworkMapDashboard.jsx @@ -29,6 +29,8 @@ export default function NetworkMapDashboard() { const [servers, setServers] = useState([]); const [connections, setConnections] = useState([]); const [pingMap, setPingMap] = useState({}); + const pingTimestampsRef = useRef({}); + const [pingCacheSeconds, setPingCacheSeconds] = useState(0); const [loading, setLoading] = useState(true); const [pingLoading, setPingLoading] = useState(false); const [error, setError] = useState(null); @@ -39,13 +41,20 @@ export default function NetworkMapDashboard() { setLoading(true); setError(null); try { - const [serversRes, configRes] = await Promise.all([ + const [serversRes, configRes, uiSettingsRes] = await Promise.all([ api.get('/servers'), api.get('/network-config').catch(() => ({ data: null })), + api.get('/ui-settings').catch(() => ({ data: {} })), ]); const serversList = Array.isArray(serversRes?.data) ? serversRes.data : []; const config = configRes?.data || {}; const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; + const uiSettings = uiSettingsRes?.data || {}; + const ttlSeconds = Math.max( + 0, + parseInt(uiSettings.networkMapPingCacheSeconds, 10) || 0 + ); + setPingCacheSeconds(ttlSeconds); const getServer = (serverId) => serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId); @@ -88,7 +97,7 @@ export default function NetworkMapDashboard() { pingAbortRef.current = false; setPingLoading(true); const key = (a, b) => `${a}:${b}`; - const newMap = {}; + const ttlMs = Math.max(0, (pingCacheSeconds || 0) * 1000); const tasks = []; connections.forEach((c) => { @@ -102,24 +111,42 @@ export default function NetworkMapDashboard() { const canPingTo = srcTo && ['jumphost', 'home'].includes(String(srcTo.type || '').toLowerCase()) && c.internalToFrom; - // Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config: - // server1 -> remoteIp, server2 -> localIp. serverId — ключ (id/dns/ip) как в servers.json. + // Проверка кеша: если TTL не истёк, не запускаем новый запрос + const keyFromTo = key(from, to); + const keyToFrom = key(to, from); + const now = Date.now(); + if (ttlMs > 0) { + const tsFrom = pingTimestampsRef.current[keyFromTo]; + if (tsFrom && now - tsFrom < ttlMs) { + // уже есть актуальное значение для from->to + } else if (!canPingFrom) { + // если пинговать нельзя, но кеша нет — оставляем как есть + } + const tsTo = pingTimestampsRef.current[keyToFrom]; + if (tsTo && now - tsTo < ttlMs) { + // уже есть актуальное значение для to->from + } else if (!canPingTo) { + // нельзя пинговать и кеша нет — пропускаем + } + } + + // Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config if (canPingFrom) { tasks.push(async () => { if (pingAbortRef.current) return; try { const { data } = await api.post('/mikrotik/ping', { serverId: c.fromKey, - // ВАЖНО: передаём внутренний IP и как target, и как gatewayIp, - // чтобы backend мог по gatewayIp найти нужный tunnel interface. target: c.internalFromTo, gatewayIp: c.internalFromTo, count: 3, }); const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; - return { key: key(from, to), ms }; + pingTimestampsRef.current[keyFromTo] = Date.now(); + setPingMap((prev) => ({ ...prev, [keyFromTo]: ms })); } catch { - return { key: key(from, to), ms: null }; + pingTimestampsRef.current[keyFromTo] = Date.now(); + setPingMap((prev) => ({ ...prev, [keyFromTo]: null })); } }); } @@ -130,29 +157,25 @@ export default function NetworkMapDashboard() { try { const { data } = await api.post('/mikrotik/ping', { serverId: c.toKey, - // Аналогично для обратного направления target: c.internalToFrom, gatewayIp: c.internalToFrom, count: 3, }); const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; - return { key: key(to, from), ms }; + pingTimestampsRef.current[keyToFrom] = Date.now(); + setPingMap((prev) => ({ ...prev, [keyToFrom]: ms })); } catch { - return { key: key(to, from), ms: null }; + pingTimestampsRef.current[keyToFrom] = Date.now(); + setPingMap((prev) => ({ ...prev, [keyToFrom]: null })); } }); } }); - const results = await runWithLimit(tasks, 4); + await runWithLimit(tasks, 4); if (pingAbortRef.current) return; - results.forEach((r) => { - const v = r?.value; - if (v && v.key != null) newMap[v.key] = v.ms ?? null; - }); - setPingMap((prev) => ({ ...prev, ...newMap })); setPingLoading(false); - }, [connections]); + }, [connections, servers, pingCacheSeconds]); useEffect(() => { fetchData(); diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index 5063e73..06ab687 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -68,6 +68,7 @@ export default function SettingsPage() { const [baseAS, setBaseAS] = useState('65001'); const [pingDomain, setPingDomain] = useState(''); const [pingCacheMinutes, setPingCacheMinutes] = useState(''); + const [networkMapPingCacheSeconds, setNetworkMapPingCacheSeconds] = useState(''); const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState(''); const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState(''); const [pingServicesSource, setPingServicesSource] = useState('web'); @@ -206,6 +207,11 @@ export default function SettingsPage() { setPingCacheMinutes( data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : '' ); + setNetworkMapPingCacheSeconds( + data?.networkMapPingCacheSeconds != null + ? String(data.networkMapPingCacheSeconds) + : '' + ); setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || '')); setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || '')); setPingServicesSource( @@ -300,6 +306,10 @@ export default function SettingsPage() { baseAS: String(baseAS || '65001').trim(), pingDomain: String(pingDomain || '').trim(), pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0), + networkMapPingCacheSeconds: Math.max( + 0, + parseInt(networkMapPingCacheSeconds, 10) || 0 + ), ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(), ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(), pingServicesSource: @@ -528,6 +538,19 @@ export default function SettingsPage() { min={0} /> +
+ +
)}