From 80de7160b93b508b2b0cac33db04a26ecf6ac3d2 Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 24 Feb 2026 22:23:12 +0700 Subject: [PATCH] refactor(alerts): consolidate tunnel alert settings into a single structure with enhanced UI controls --- backend/routes/alertsRoutes.js | 202 ++++++++++-------- frontend/src/SettingsPage.jsx | 362 ++++++++++++++++++++++----------- 2 files changed, 359 insertions(+), 205 deletions(-) diff --git a/backend/routes/alertsRoutes.js b/backend/routes/alertsRoutes.js index d5eddb3..70f6333 100644 --- a/backend/routes/alertsRoutes.js +++ b/backend/routes/alertsRoutes.js @@ -18,10 +18,8 @@ const DEFAULT_ALERT_SETTINGS = { highCpu: { enabled: true, thresholdPercent: 85, durationMinutes: 10 }, highRam: { enabled: true, thresholdPercent: 85, durationMinutes: 10 }, highHdd: { enabled: true, thresholdPercent: 90, durationMinutes: 10 }, - /** Порог по пингу туннелей (карта сети) */ - tunnelHighPing: { enabled: false, thresholdMs: 0 }, - /** Пороги по скорости туннелей (карта сети) */ - tunnelLowSpeed: { enabled: false, minDownloadMbps: 0, minUploadMbps: 0 }, + /** Индивидуальные пороги по туннелям (карта сети) */ + tunnelThresholds: [], }; function getAlertSettings(uiSettings) { @@ -32,8 +30,7 @@ function getAlertSettings(uiSettings) { highCpu: { ...DEFAULT_ALERT_SETTINGS.highCpu, ...raw.highCpu }, highRam: { ...DEFAULT_ALERT_SETTINGS.highRam, ...raw.highRam }, highHdd: { ...DEFAULT_ALERT_SETTINGS.highHdd, ...raw.highHdd }, - tunnelHighPing: { ...DEFAULT_ALERT_SETTINGS.tunnelHighPing, ...raw.tunnelHighPing }, - tunnelLowSpeed: { ...DEFAULT_ALERT_SETTINGS.tunnelLowSpeed, ...raw.tunnelLowSpeed }, + tunnelThresholds: Array.isArray(raw.tunnelThresholds) ? raw.tunnelThresholds : [], }; } @@ -193,13 +190,15 @@ async function getAlerts(req, res) { } }); - // 3) Туннели (карта сети): высокий пинг и/или низкая скорость - const tunnelPingSettings = settings.tunnelHighPing || {}; - const tunnelSpeedSettings = settings.tunnelLowSpeed || {}; - const tunnelPingEnabled = Boolean(tunnelPingSettings.enabled); - const tunnelSpeedEnabled = Boolean(tunnelSpeedSettings.enabled); + // 3) Туннели (карта сети): индивидуальные пороги по пингу и скорости + const rawTunnelThresholds = Array.isArray(settings.tunnelThresholds) + ? settings.tunnelThresholds + : []; + const tunnelThresholds = rawTunnelThresholds.filter( + (t) => t && (t.fromKey || t.toKey) && t.enabled !== false + ); - if (tunnelPingEnabled || tunnelSpeedEnabled) { + if (tunnelThresholds.length > 0) { let cache = null; let connectionsData = null; try { @@ -221,88 +220,127 @@ async function getAlerts(req, res) { const pingMap = cache.pingMap && typeof cache.pingMap === 'object' ? cache.pingMap : {}; const speedMap = cache.speedMap && typeof cache.speedMap === 'object' ? cache.speedMap : {}; - const thPingMs = Math.max(1, Number(tunnelPingSettings.thresholdMs) || 0); - const minDownMbps = Math.max(0, Number(tunnelSpeedSettings.minDownloadMbps) || 0); - const minUpMbps = Math.max(0, Number(tunnelSpeedSettings.minUploadMbps) || 0); + const serversByIp = new Map(); + servers.forEach((s) => { + if (s && s.ip) { + serversByIp.set(String(s.ip), s); + } + }); - if (thPingMs > 0 || minDownMbps > 0 || minUpMbps > 0) { - const serversByIp = new Map(); - servers.forEach((s) => { - if (s && s.ip) { - serversByIp.set(String(s.ip), s); - } + const thresholdsByKey = new Map(); + const makeKey = (fromKey, toKey, iface) => { + const a = String(fromKey || ''); + const b = String(toKey || ''); + const pair = [a, b].sort().join('__'); + return `${pair}::${iface || ''}`; + }; + + tunnelThresholds.forEach((t) => { + const key = makeKey(t.fromKey, t.toKey, t.interfaceName || ''); + const maxPingMs = + typeof t.maxPingMs === 'number' + ? t.maxPingMs + : t.thresholdMs != null + ? Number(t.thresholdMs) || 0 + : 0; + const minDownloadMbps = + typeof t.minDownloadMbps === 'number' + ? t.minDownloadMbps + : t.minDownMbps != null + ? Number(t.minDownMbps) || 0 + : 0; + const minUploadMbps = + typeof t.minUploadMbps === 'number' + ? t.minUploadMbps + : t.minUpMbps != null + ? Number(t.minUpMbps) || 0 + : 0; + thresholdsByKey.set(key, { + maxPingMs, + minDownloadMbps, + minUploadMbps, }); + }); - connectionsData.connections.forEach((c) => { - const fromIp = String(c.from); - const toIp = String(c.to); - if (!fromIp || !toIp || fromIp === toIp) return; + connectionsData.connections.forEach((c) => { + const fromIp = String(c.from); + const toIp = String(c.to); + if (!fromIp || !toIp || fromIp === toIp) return; - const fromServer = serversByIp.get(fromIp) || null; - const toServer = serversByIp.get(toIp) || null; - const fromName = fromServer?.name || fromServer?.dns || fromIp; - const toName = toServer?.name || toServer?.dns || toIp; + const fromServer = serversByIp.get(fromIp) || null; + const toServer = serversByIp.get(toIp) || null; + const fromName = fromServer?.name || fromServer?.dns || fromIp; + const toName = toServer?.name || toServer?.dns || toIp; - const pingMapKey = edgePingKey(fromIp, toIp); - const pingMs = typeof pingMap[pingMapKey] === 'number' ? pingMap[pingMapKey] : null; + const fromKey = c.fromKey || fromIp; + const toKey = c.toKey || toIp; + const key = makeKey(fromKey, toKey, c.interfaceName || ''); + const th = thresholdsByKey.get(key); + if (!th) return; - if (tunnelPingEnabled && thPingMs > 0 && pingMs != null && pingMs >= thPingMs) { - alerts.push({ - id: `tunnel-ping-${pingMapKey}-${thPingMs}`, - type: 'tunnel_high_ping', - severity: 'warning', - title: 'Высокий пинг по туннелю', - description: `Пинг между ${fromName} и ${toName}: ${pingMs} ms (порог ${thPingMs} ms).`, - entity: `${fromName} ⇄ ${toName}`, - entityId: `${c.fromKey || fromIp}__${c.toKey || toIp}__${c.interfaceName || ''}`, - link: '/network-map', - at: now, - }); - } + const maxPingMs = Math.max(0, Number(th.maxPingMs) || 0); + const minDownMbps = Math.max(0, Number(th.minDownloadMbps) || 0); + const minUpMbps = Math.max(0, Number(th.minUploadMbps) || 0); - if (tunnelSpeedEnabled && (minDownMbps > 0 || minUpMbps > 0) && c.fromKey && c.toKey) { - const sk = speedKey(c.fromKey, c.toKey); - const speed = speedMap[sk]; - if (speed && typeof speed === 'object') { - const downMbps = - typeof speed.tcpDownloadBps === 'number' ? speed.tcpDownloadBps / 1e6 : null; - const upMbps = - typeof speed.tcpUploadBps === 'number' ? speed.tcpUploadBps / 1e6 : null; + const pingMapKey = edgePingKey(fromIp, toIp); + const pingMs = typeof pingMap[pingMapKey] === 'number' ? pingMap[pingMapKey] : null; - const downTooLow = minDownMbps > 0 && (downMbps == null || downMbps < minDownMbps); - const upTooLow = minUpMbps > 0 && (upMbps == null || upMbps < minUpMbps); + if (maxPingMs > 0 && pingMs != null && pingMs >= maxPingMs) { + alerts.push({ + id: `tunnel-ping-${pingMapKey}-${maxPingMs}`, + type: 'tunnel_high_ping', + severity: 'warning', + title: 'Высокий пинг по туннелю', + description: `Пинг между ${fromName} и ${toName}: ${pingMs} ms (порог ${maxPingMs} ms).`, + entity: `${fromName} ⇄ ${toName}`, + entityId: `${c.fromKey || fromIp}__${c.toKey || toIp}__${c.interfaceName || ''}`, + link: '/network-map', + at: now, + }); + } - if (downTooLow || upTooLow) { - const parts = []; - if (downTooLow) { - parts.push( - `↓ ${downMbps != null ? downMbps.toFixed(1) : '—'} Мбит/с (порог ${minDownMbps} Мбит/с)` - ); - } - if (upTooLow) { - parts.push( - `↑ ${upMbps != null ? upMbps.toFixed(1) : '—'} Мбит/с (порог ${minUpMbps} Мбит/с)` - ); - } - const ifaceLabel = c.interfaceName ? `, интерфейс ${c.interfaceName}` : ''; - alerts.push({ - id: `tunnel-speed-${sk}-${minDownMbps}-${minUpMbps}`, - type: 'tunnel_low_speed', - severity: 'warning', - title: 'Низкая скорость по туннелю', - description: `Туннель между ${fromName} и ${toName}${ifaceLabel}: ${parts.join( - '; ' - )}.`, - entity: `${fromName} ⇄ ${toName}`, - entityId: `${c.fromKey || fromIp}__${c.toKey || toIp}__${c.interfaceName || ''}`, - link: '/network-map', - at: now, - }); + if ((minDownMbps > 0 || minUpMbps > 0) && c.fromKey && c.toKey) { + const sk = speedKey(c.fromKey, c.toKey); + const speed = speedMap[sk]; + if (speed && typeof speed === 'object') { + const downMbps = + typeof speed.tcpDownloadBps === 'number' ? speed.tcpDownloadBps / 1e6 : null; + const upMbps = + typeof speed.tcpUploadBps === 'number' ? speed.tcpUploadBps / 1e6 : null; + + const downTooLow = minDownMbps > 0 && (downMbps == null || downMbps < minDownMbps); + const upTooLow = minUpMbps > 0 && (upMbps == null || upMbps < minUpMbps); + + if (downTooLow || upTooLow) { + const parts = []; + if (downTooLow) { + parts.push( + `↓ ${downMbps != null ? downMbps.toFixed(1) : '—'} Мбит/с (порог ${minDownMbps} Мбит/с)` + ); } + if (upTooLow) { + parts.push( + `↑ ${upMbps != null ? upMbps.toFixed(1) : '—'} Мбит/с (порог ${minUpMbps} Мбит/с)` + ); + } + const ifaceLabel = c.interfaceName ? `, интерфейс ${c.interfaceName}` : ''; + alerts.push({ + id: `tunnel-speed-${sk}-${minDownMbps}-${minUpMbps}`, + type: 'tunnel_low_speed', + severity: 'warning', + title: 'Низкая скорость по туннелю', + description: `Туннель между ${fromName} и ${toName}${ifaceLabel}: ${parts.join( + '; ' + )}.`, + entity: `${fromName} ⇄ ${toName}`, + entityId: `${c.fromKey || fromIp}__${c.toKey || toIp}__${c.interfaceName || ''}`, + link: '/network-map', + at: now, + }); } } - }); - } + } + }); } } diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index c31f390..3ad5207 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -114,12 +114,9 @@ export default function SettingsPage() { const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true); const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90'); const [alertHighHddDurationMinutes, setAlertHighHddDurationMinutes] = useState('10'); - const [alertTunnelHighPingEnabled, setAlertTunnelHighPingEnabled] = useState(false); - const [alertTunnelHighPingThresholdMs, setAlertTunnelHighPingThresholdMs] = useState('0'); - const [alertTunnelLowSpeedEnabled, setAlertTunnelLowSpeedEnabled] = useState(false); - const [alertTunnelLowSpeedMinDownload, setAlertTunnelLowSpeedMinDownload] = useState('0'); - const [alertTunnelLowSpeedMinUpload, setAlertTunnelLowSpeedMinUpload] = useState('0'); const [serversList, setServersList] = useState([]); + const [tunnelConnections, setTunnelConnections] = useState([]); + const [tunnelThresholds, setTunnelThresholds] = useState([]); const [sidebarSearch, setSidebarSearch] = useState(''); const [activeSection, setActiveSection] = useState(() => { const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || ''; @@ -234,9 +231,10 @@ export default function SettingsPage() { (async () => { try { - const [settingsRes, serversRes] = await Promise.all([ + const [settingsRes, serversRes, networkRes] = await Promise.all([ api.get('/ui-settings'), api.get('/servers').catch(() => ({ data: [] })), + api.get('/network-config').catch(() => ({ data: null })), ]); const data = settingsRes?.data || {}; setRawSettings(data); @@ -324,6 +322,37 @@ export default function SettingsPage() { })) : [] ); + const serversData = Array.isArray(serversRes?.data) ? serversRes.data : []; + setServersList(serversData); + + // Построить список туннелей (как в NetworkMapDashboard / планировщике карты сети) + const config = networkRes?.data || {}; + const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : []; + const getServer = (serverId) => + serversData.find( + (s) => s.id === serverId || s.ip === serverId || s.dns === serverId + ); + const tunnelConns = []; + tunnelInterfaces.forEach((iface) => { + if (!iface.serverId || !iface.serverId2) return; + const s1 = getServer(iface.serverId); + const s2 = getServer(iface.serverId2); + if (!s1 || !s2 || s1.ip === s2.ip) return; + const s1Key = s1.id || s1.dns || s1.ip; + const s2Key = s2.id || s2.dns || s2.ip; + if (!s1Key || !s2Key) return; + tunnelConns.push({ + from: s1.ip, + to: s2.ip, + fromKey: s1Key, + toKey: s2Key, + interfaceName: iface.name || '', + fromLabel: s1.name || s1.dns || s1.ip || s1.id || 'Сервер', + toLabel: s2.name || s2.dns || s2.ip || s2.id || 'Сервер', + }); + }); + setTunnelConnections(tunnelConns); + const a = data?.alertSettings || {}; setAlertServerOffline(a.serverOffline?.enabled !== false); setAlertServerOfflineMinutes( @@ -356,23 +385,40 @@ export default function SettingsPage() { setAlertHighHddDurationMinutes( a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10' ); - setAlertTunnelHighPingEnabled(a.tunnelHighPing?.enabled === true); - setAlertTunnelHighPingThresholdMs( - a.tunnelHighPing?.thresholdMs != null ? String(a.tunnelHighPing.thresholdMs) : '0' - ); - setAlertTunnelLowSpeedEnabled(a.tunnelLowSpeed?.enabled === true); - setAlertTunnelLowSpeedMinDownload( - a.tunnelLowSpeed?.minDownloadMbps != null - ? String(a.tunnelLowSpeed.minDownloadMbps) - : '0' - ); - setAlertTunnelLowSpeedMinUpload( - a.tunnelLowSpeed?.minUploadMbps != null ? String(a.tunnelLowSpeed.minUploadMbps) : '0' + + const rawTunnelThresholds = Array.isArray(a.tunnelThresholds) ? a.tunnelThresholds : []; + setTunnelThresholds( + rawTunnelThresholds + .filter((t) => t && (t.fromKey || t.toKey)) + .map((t) => ({ + fromKey: String(t.fromKey || '').trim(), + toKey: String(t.toKey || '').trim(), + interfaceName: t.interfaceName != null ? String(t.interfaceName) : '', + enabled: t.enabled !== false, + maxPingMs: + t.maxPingMs != null + ? String(t.maxPingMs) + : t.thresholdMs != null + ? String(t.thresholdMs) + : '', + minDownloadMbps: + t.minDownloadMbps != null + ? String(t.minDownloadMbps) + : t.minDownMbps != null + ? String(t.minDownMbps) + : '', + minUploadMbps: + t.minUploadMbps != null + ? String(t.minUploadMbps) + : t.minUpMbps != null + ? String(t.minUpMbps) + : '', + })) ); + const e = settingsRes?.headers?.etag || settingsRes?.headers?.ETag || ''; setEtag(e ? String(e) : ''); - setServersList(Array.isArray(serversRes?.data) ? serversRes.data : []); } catch (e) { setError('Не удалось загрузить настройки'); } finally { @@ -405,6 +451,46 @@ export default function SettingsPage() { } }; + const tunnelKey = (fromKey, toKey, interfaceName) => { + const a = String(fromKey || ''); + const b = String(toKey || ''); + const pair = [a, b].sort().join('__'); + return `${pair}::${interfaceName || ''}`; + }; + + const findTunnelThreshold = (conn) => { + const key = tunnelKey(conn.fromKey || conn.from, conn.toKey || conn.to, conn.interfaceName || ''); + return tunnelThresholds.find( + (t) => tunnelKey(t.fromKey, t.toKey, t.interfaceName) === key + ); + }; + + const upsertTunnelThreshold = (conn, patch) => { + setTunnelThresholds((prev) => { + const key = tunnelKey(conn.fromKey || conn.from, conn.toKey || conn.to, conn.interfaceName || ''); + const idx = prev.findIndex( + (t) => tunnelKey(t.fromKey, t.toKey, t.interfaceName) === key + ); + const base = + idx >= 0 + ? prev[idx] + : { + fromKey: String(conn.fromKey || conn.from || '').trim(), + toKey: String(conn.toKey || conn.to || '').trim(), + interfaceName: conn.interfaceName || '', + enabled: true, + maxPingMs: '', + minDownloadMbps: '', + minUploadMbps: '', + }; + const nextItem = { ...base, ...patch }; + const next = [...prev]; + if (idx >= 0) next[idx] = nextItem; + else next.push(nextItem); + return next; + }); + }; + const onSave = async () => { setError(''); setSuccess(''); @@ -483,7 +569,7 @@ export default function SettingsPage() { interfaceName: p.interfaceName, })) : [], - alertSettings: { + alertSettings: { serverOffline: { enabled: alertServerOffline, offlineMinutes: Math.max( @@ -531,24 +617,28 @@ export default function SettingsPage() { Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10) ), }, - tunnelHighPing: { - enabled: alertTunnelHighPingEnabled, - thresholdMs: Math.max( - 1, - Math.min(5000, parseInt(alertTunnelHighPingThresholdMs, 10) || 0) - ), - }, - tunnelLowSpeed: { - enabled: alertTunnelLowSpeedEnabled, - minDownloadMbps: Math.max( - 0, - Math.min(10000, parseInt(alertTunnelLowSpeedMinDownload, 10) || 0) - ), - minUploadMbps: Math.max( - 0, - Math.min(10000, parseInt(alertTunnelLowSpeedMinUpload, 10) || 0) - ), - }, + tunnelThresholds: Array.isArray(tunnelThresholds) + ? tunnelThresholds + .filter((t) => { + const fromKey = String(t.fromKey || '').trim(); + const toKey = String(t.toKey || '').trim(); + if (!fromKey || !toKey) return false; + if (t.enabled === false) return false; + const maxPing = parseInt(t.maxPingMs, 10) || 0; + const minDown = parseInt(t.minDownloadMbps, 10) || 0; + const minUp = parseInt(t.minUploadMbps, 10) || 0; + return maxPing > 0 || minDown > 0 || minUp > 0; + }) + .map((t) => ({ + fromKey: String(t.fromKey || '').trim(), + toKey: String(t.toKey || '').trim(), + interfaceName: String(t.interfaceName || '').trim() || null, + enabled: t.enabled !== false, + maxPingMs: Math.max(0, parseInt(t.maxPingMs, 10) || 0), + minDownloadMbps: Math.max(0, parseInt(t.minDownloadMbps, 10) || 0), + minUploadMbps: Math.max(0, parseInt(t.minUploadMbps, 10) || 0), + })) + : [], }, }; const payload = { settings: mergedSettings, etag }; @@ -1399,94 +1489,120 @@ export default function SettingsPage() { - {/* Пинг по туннелям (карта сети) */} -
-
-
Пинг по туннелям
+ {/* Туннели (карта сети): индивидуальные пороги */} +
+
Туннели (карта сети)
+
+ Для каждого туннеля можно задать собственные пороги по пингу и скорости. Используются данные + с планировщика карты сети и раздела «Сетевые настройки». +
+ {tunnelConnections.length === 0 && (
- Срабатывает, если средний пинг по туннелю на карте сети превышает указанный порог. - Применяется ко всем туннелям из раздела «Сетевые настройки». + Нет настроенных туннелей. Добавьте связи в разделе «Сетевые настройки».
-
-
-
- setAlertTunnelHighPingThresholdMs(e.target.value)} - disabled={saving || !alertTunnelHighPingEnabled} - /> - ms + )} + {tunnelConnections.length > 0 && ( +
+ + + + + + + + + + + + + {tunnelConnections.map((conn) => { + const thr = findTunnelThreshold(conn) || {}; + const enabled = thr.enabled === undefined ? false : thr.enabled; + const maxPingMs = thr.maxPingMs ?? ''; + const minDown = thr.minDownloadMbps ?? ''; + const minUp = thr.minUploadMbps ?? ''; + return ( + + + + + + + + + ); + })} + +
ТуннельИнтерфейсМакс. пинг (ms)Мин. ↓ (Мбит/с)Мин. ↑ (Мбит/с)Вкл
+
+ + {conn.fromLabel} ⇄ {conn.toLabel} + +
+
+ {conn.from} ⇄ {conn.to} +
+
+ {conn.interfaceName ? ( + {conn.interfaceName} + ) : ( + + )} + + + upsertTunnelThreshold(conn, { maxPingMs: e.target.value }) + } + disabled={saving || !enabled} + /> + + + upsertTunnelThreshold(conn, { + minDownloadMbps: e.target.value, + }) + } + disabled={saving || !enabled} + /> + + + upsertTunnelThreshold(conn, { + minUploadMbps: e.target.value, + }) + } + disabled={saving || !enabled} + /> + +
+ + upsertTunnelThreshold(conn, { enabled: e.target.checked }) + } + disabled={saving} + /> +
+
-
- setAlertTunnelHighPingEnabled(e.target.checked)} - disabled={saving} - /> - -
-
-
- - {/* Скорость по туннелям (карта сети) */} -
-
-
Скорость по туннелям
-
- Срабатывает, если измеренная скорость по туннелю (download и/или upload) ниже указанных - порогов. Используются последние результаты планировщика карты сети. -
-
-
-
- - setAlertTunnelLowSpeedMinDownload(e.target.value)} - disabled={saving || !alertTunnelLowSpeedEnabled} - /> - Мбит/с -
-
- - setAlertTunnelLowSpeedMinUpload(e.target.value)} - disabled={saving || !alertTunnelLowSpeedEnabled} - /> - Мбит/с -
-
- setAlertTunnelLowSpeedEnabled(e.target.checked)} - disabled={saving} - /> - -
-
+ )}