diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index c73561d..ba920f4 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -737,19 +737,44 @@ async function tracerouteViaGateway(req, res) { return m ? Number(m[1]) : null; }; - const hops = rows.map((r, index) => ({ - hop: r.hop != null ? Number(r.hop) : index + 1, - host: r.host || r.address || '', - avgMs: parseMs(r['avg-rtt'] || r.time || r.avg), - bestMs: parseMs(r['best-rtt'] || r['min-rtt']), - worstMs: parseMs(r['worst-rtt'] || r['max-rtt']), - loss: - r['packet-loss'] != null - ? Number(String(r['packet-loss']).replace('%', '')) - : null, - status: r.status || '', - raw: r, - })); + // MikroTik REST для traceroute может отдавать несколько строк + // по одному и тому же хопу. Дедуплицируем по host+avg+loss и + // выбрасываем пустые служебные строки без host. + const hops = []; + const seen = new Set(); + + for (const r of rows) { + const host = r.host || r.address || ''; + + // Пропускаем полностью пустые строки без адреса и статуса + if (!host && !r.status) continue; + + const avgField = r.avg || r['avg-rtt'] || r.time; + const lossField = r['packet-loss']; + const key = `${host}|${avgField || ''}|${lossField || ''}`; + + if (host && seen.has(key)) continue; + if (host) seen.add(key); + + hops.push({ + hop: null, // заполним ниже последовательной нумерацией + host, + avgMs: parseMs(avgField), + bestMs: parseMs(r.best || r['best-rtt'] || r['min-rtt']), + worstMs: parseMs(r.worst || r['worst-rtt'] || r['max-rtt']), + loss: + lossField != null + ? Number(String(lossField).replace('%', '')) + : null, + status: r.status || '', + raw: r, + }); + } + + // Финальная нумерация хопов 1..N + hops.forEach((h, idx) => { + h.hop = idx + 1; + }); return res.json({ ok: true, hops }); } catch (error) {