diff --git a/frontend/src/NetworkMapUnifi.jsx b/frontend/src/NetworkMapUnifi.jsx index ccc2c73..977faf6 100644 --- a/frontend/src/NetworkMapUnifi.jsx +++ b/frontend/src/NetworkMapUnifi.jsx @@ -287,6 +287,7 @@ export default function NetworkMapUnifi({ to, p1, p2, + pingMs: typeof ms === 'number' ? ms : null, pingLabel, speedLabel, compactEdgeLabel, @@ -477,16 +478,114 @@ export default function NetworkMapUnifi({ }; }, [computeCurvePoints, size.w, size.h]); + const problematicNodes = useMemo(() => { + const byIp = new Map(); + servers.forEach((s) => { + byIp.set(String(s.ip), { + ip: String(s.ip), + dns: s.dns || '', + type: String(s.type || '').toLowerCase(), + linkCount: 0, + staleCount: 0, + maxPingMs: null, + minSpeedMbps: null, + }); + }); + + (connections || []).forEach((c) => { + const from = byIp.get(String(c.from)); + const to = byIp.get(String(c.to)); + if (from) from.linkCount += 1; + if (to) to.linkCount += 1; + }); + + routedEdges.forEach((e) => { + [e.from, e.to].forEach((nodeIp) => { + const node = byIp.get(String(nodeIp)); + if (!node) return; + if (e.stale) node.staleCount += 1; + if (typeof e.pingMs === 'number') { + node.maxPingMs = node.maxPingMs == null ? e.pingMs : Math.max(node.maxPingMs, e.pingMs); + } + const down = e.speed?.tcpDownloadBps != null ? e.speed.tcpDownloadBps / 1_000_000 : null; + const up = e.speed?.tcpUploadBps != null ? e.speed.tcpUploadBps / 1_000_000 : null; + const edgeMin = down != null && up != null ? Math.min(down, up) : (down ?? up); + if (edgeMin != null && Number.isFinite(edgeMin)) { + node.minSpeedMbps = node.minSpeedMbps == null ? edgeMin : Math.min(node.minSpeedMbps, edgeMin); + } + }); + }); + + return Array.from(byIp.values()) + .map((n) => { + const issues = []; + let riskScore = 0; + + if (n.maxPingMs != null) { + if (n.maxPingMs >= 120) { + issues.push('Очень высокий ping'); + riskScore += 3; + } else if (n.maxPingMs >= 80) { + issues.push('Повышенный ping'); + riskScore += 2; + } + } + + if (n.minSpeedMbps != null) { + if (n.minSpeedMbps < 40) { + issues.push('Низкая скорость'); + riskScore += 3; + } else if (n.minSpeedMbps < 80) { + issues.push('Скорость ниже нормы'); + riskScore += 2; + } + } + + if (n.staleCount > 0) { + issues.push('Есть устаревшие метрики'); + riskScore += n.staleCount >= 2 ? 2 : 1; + } + + if (n.linkCount >= 7) { + issues.push('Высокая связанность'); + riskScore += 1; + } + + return { + ...n, + issues, + riskScore, + nodeLabel: n.dns ? String(n.dns).split('.')[0] : n.ip, + }; + }) + .filter((n) => n.issues.length > 0) + .sort((a, b) => + b.riskScore - a.riskScore || + (b.maxPingMs ?? -1) - (a.maxPingMs ?? -1) || + (a.minSpeedMbps ?? Infinity) - (b.minSpeedMbps ?? Infinity) + ); + }, [servers, connections, routedEdges]); + + const criticalNodes = useMemo( + () => problematicNodes.filter((n) => n.riskScore >= 4).slice(0, 8), + [problematicNodes] + ); + const warningNodes = useMemo( + () => problematicNodes.filter((n) => n.riskScore >= 1 && n.riskScore < 4).slice(0, 12), + [problematicNodes] + ); + if (servers.length === 0) return null; return ( + <>
+
+
+
+
+

Критичные узлы

+
+
+ + + + + + + + + + + + + {criticalNodes.length === 0 ? ( + + + + ) : ( + criticalNodes.map((n) => ( + + + + + + + + + )) + )} + +
УзелТипPing maxSpeed minСвязиПроблемы
Критичных узлов не найдено
+
{n.nodeLabel}
+
{n.ip}
+
+ + {String(n.type || 'node').toUpperCase()} + + {n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}{n.linkCount}{n.issues.slice(0, 2).join(', ')}
+
+
+
+
+
+
+

Требуют внимания

+
+
+ + + + + + + + + + + + {warningNodes.length === 0 ? ( + + + + ) : ( + warningNodes.map((n) => ( + + + + + + + + )) + )} + +
УзелPing maxSpeed minStaleПроблемы
Узлов с предупреждениями не найдено
+
{n.nodeLabel}
+
{n.ip}
+
{n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}{n.staleCount}{n.issues.slice(0, 2).join(', ')}
+
+
+
+
+ ); }