diff --git a/backend/routes/alertsRoutes.js b/backend/routes/alertsRoutes.js index 49bb3df..d5eddb3 100644 --- a/backend/routes/alertsRoutes.js +++ b/backend/routes/alertsRoutes.js @@ -8,6 +8,8 @@ const { readServersFromS3 } = require('./serversRoutes'); const { checkOneServerFast, loadUiSettingsSync } = require('./miscRoutes'); const { getResourceStatsData } = require('./resourceStatsRoutes'); const { getUptimeCacheData } = require('./mikrotikConfigRoutes'); +const { buildConnections } = require('../services/networkMapScheduler'); +const { readS3TextObject } = require('../services/s3Service'); /** Значения по умолчанию для настроек оповещений. */ const DEFAULT_ALERT_SETTINGS = { @@ -16,6 +18,10 @@ 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 }, }; function getAlertSettings(uiSettings) { @@ -26,9 +32,19 @@ 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 }, }; } +function edgePingKey(a, b) { + return [String(a), String(b)].sort().join(':'); +} + +function speedKey(key1, key2) { + return [String(key1), String(key2)].sort().join(':'); +} + /** * GET /api/alerts * Возвращает список активных оповещений (учёт настроек из ui-settings.alertSettings). @@ -177,6 +193,119 @@ async function getAlerts(req, res) { } }); + // 3) Туннели (карта сети): высокий пинг и/или низкая скорость + const tunnelPingSettings = settings.tunnelHighPing || {}; + const tunnelSpeedSettings = settings.tunnelLowSpeed || {}; + const tunnelPingEnabled = Boolean(tunnelPingSettings.enabled); + const tunnelSpeedEnabled = Boolean(tunnelSpeedSettings.enabled); + + if (tunnelPingEnabled || tunnelSpeedEnabled) { + let cache = null; + let connectionsData = null; + try { + const [rawCache, built] = await Promise.all([ + readS3TextObject('network-map-cache/latest.json').catch(() => null), + buildConnections().catch(() => null), + ]); + if (rawCache?.body) { + const parsed = JSON.parse(rawCache.body); + if (parsed && typeof parsed === 'object') cache = parsed; + } + if (built && typeof built === 'object') connectionsData = built; + } catch (e) { + // Ошибки при чтении кеша/конфигурации не должны ломать остальные оповещения + console.warn('alerts: network-map-cache read failed', e?.message || e); + } + + if (cache && connectionsData && Array.isArray(connectionsData.connections)) { + 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); + + if (thPingMs > 0 || minDownMbps > 0 || minUpMbps > 0) { + const serversByIp = new Map(); + servers.forEach((s) => { + if (s && s.ip) { + serversByIp.set(String(s.ip), s); + } + }); + + 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 pingMapKey = edgePingKey(fromIp, toIp); + const pingMs = typeof pingMap[pingMapKey] === 'number' ? pingMap[pingMapKey] : null; + + 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, + }); + } + + 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 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, + }); + } + } + } + }); + } + } + } + return res.json({ alerts, total: alerts.length, diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index 3668e55..c31f390 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -114,6 +114,11 @@ 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 [sidebarSearch, setSidebarSearch] = useState(''); const [activeSection, setActiveSection] = useState(() => { @@ -321,18 +326,49 @@ export default function SettingsPage() { ); const a = data?.alertSettings || {}; setAlertServerOffline(a.serverOffline?.enabled !== false); - setAlertServerOfflineMinutes(a.serverOffline?.offlineMinutes != null ? String(a.serverOffline.offlineMinutes) : '5'); + setAlertServerOfflineMinutes( + a.serverOffline?.offlineMinutes != null ? String(a.serverOffline.offlineMinutes) : '5' + ); setAlertMikrotikUnreachable(a.mikrotikUnreachable?.enabled !== false); - setAlertMikrotikUnreachableMinutes(a.mikrotikUnreachable?.durationMinutes != null ? String(a.mikrotikUnreachable.durationMinutes) : '5'); + setAlertMikrotikUnreachableMinutes( + a.mikrotikUnreachable?.durationMinutes != null + ? String(a.mikrotikUnreachable.durationMinutes) + : '5' + ); setAlertHighCpuEnabled(a.highCpu?.enabled !== false); - setAlertHighCpuThreshold(a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85'); - setAlertHighCpuDurationMinutes(a.highCpu?.durationMinutes != null ? String(a.highCpu.durationMinutes) : '10'); + setAlertHighCpuThreshold( + a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85' + ); + setAlertHighCpuDurationMinutes( + a.highCpu?.durationMinutes != null ? String(a.highCpu.durationMinutes) : '10' + ); setAlertHighRamEnabled(a.highRam?.enabled !== false); - setAlertHighRamThreshold(a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85'); - setAlertHighRamDurationMinutes(a.highRam?.durationMinutes != null ? String(a.highRam.durationMinutes) : '10'); + setAlertHighRamThreshold( + a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85' + ); + setAlertHighRamDurationMinutes( + a.highRam?.durationMinutes != null ? String(a.highRam.durationMinutes) : '10' + ); setAlertHighHddEnabled(a.highHdd?.enabled !== false); - setAlertHighHddThreshold(a.highHdd?.thresholdPercent != null ? String(a.highHdd.thresholdPercent) : '90'); - setAlertHighHddDurationMinutes(a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10'); + setAlertHighHddThreshold( + a.highHdd?.thresholdPercent != null ? String(a.highHdd.thresholdPercent) : '90' + ); + 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 e = settingsRes?.headers?.etag || settingsRes?.headers?.ETag || ''; setEtag(e ? String(e) : ''); @@ -447,29 +483,71 @@ export default function SettingsPage() { interfaceName: p.interfaceName, })) : [], - alertSettings: { + alertSettings: { serverOffline: { enabled: alertServerOffline, - offlineMinutes: Math.max(1, Math.min(1440, parseInt(alertServerOfflineMinutes, 10) || 5)), + offlineMinutes: Math.max( + 1, + Math.min(1440, parseInt(alertServerOfflineMinutes, 10) || 5) + ), }, mikrotikUnreachable: { enabled: alertMikrotikUnreachable, - durationMinutes: Math.max(1, Math.min(1440, parseInt(alertMikrotikUnreachableMinutes, 10) || 5)), + durationMinutes: Math.max( + 1, + Math.min(1440, parseInt(alertMikrotikUnreachableMinutes, 10) || 5) + ), }, highCpu: { enabled: alertHighCpuEnabled, - thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85)), - durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighCpuDurationMinutes, 10) || 10)), + thresholdPercent: Math.max( + 1, + Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85) + ), + durationMinutes: Math.max( + 1, + Math.min(1440, parseInt(alertHighCpuDurationMinutes, 10) || 10) + ), }, highRam: { enabled: alertHighRamEnabled, - thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighRamThreshold, 10) || 85)), - durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighRamDurationMinutes, 10) || 10)), + thresholdPercent: Math.max( + 1, + Math.min(100, parseInt(alertHighRamThreshold, 10) || 85) + ), + durationMinutes: Math.max( + 1, + Math.min(1440, parseInt(alertHighRamDurationMinutes, 10) || 10) + ), }, highHdd: { enabled: alertHighHddEnabled, - thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighHddThreshold, 10) || 90)), - durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)), + thresholdPercent: Math.max( + 1, + Math.min(100, parseInt(alertHighHddThreshold, 10) || 90) + ), + durationMinutes: Math.max( + 1, + 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) + ), }, }, }; @@ -1320,6 +1398,96 @@ export default function SettingsPage() { + + {/* Пинг по туннелям (карта сети) */} +
+
+
Пинг по туннелям
+
+ Срабатывает, если средний пинг по туннелю на карте сети превышает указанный порог. + Применяется ко всем туннелям из раздела «Сетевые настройки». +
+
+
+
+ setAlertTunnelHighPingThresholdMs(e.target.value)} + disabled={saving || !alertTunnelHighPingEnabled} + /> + ms +
+
+ 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} + /> + +
+
+
)}