refactor(alerts): consolidate tunnel alert settings into a single structure with enhanced UI controls
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
This commit is contained in:
+120
-82
@@ -18,10 +18,8 @@ const DEFAULT_ALERT_SETTINGS = {
|
|||||||
highCpu: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
|
highCpu: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
|
||||||
highRam: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
|
highRam: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
|
||||||
highHdd: { enabled: true, thresholdPercent: 90, durationMinutes: 10 },
|
highHdd: { enabled: true, thresholdPercent: 90, durationMinutes: 10 },
|
||||||
/** Порог по пингу туннелей (карта сети) */
|
/** Индивидуальные пороги по туннелям (карта сети) */
|
||||||
tunnelHighPing: { enabled: false, thresholdMs: 0 },
|
tunnelThresholds: [],
|
||||||
/** Пороги по скорости туннелей (карта сети) */
|
|
||||||
tunnelLowSpeed: { enabled: false, minDownloadMbps: 0, minUploadMbps: 0 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAlertSettings(uiSettings) {
|
function getAlertSettings(uiSettings) {
|
||||||
@@ -32,8 +30,7 @@ function getAlertSettings(uiSettings) {
|
|||||||
highCpu: { ...DEFAULT_ALERT_SETTINGS.highCpu, ...raw.highCpu },
|
highCpu: { ...DEFAULT_ALERT_SETTINGS.highCpu, ...raw.highCpu },
|
||||||
highRam: { ...DEFAULT_ALERT_SETTINGS.highRam, ...raw.highRam },
|
highRam: { ...DEFAULT_ALERT_SETTINGS.highRam, ...raw.highRam },
|
||||||
highHdd: { ...DEFAULT_ALERT_SETTINGS.highHdd, ...raw.highHdd },
|
highHdd: { ...DEFAULT_ALERT_SETTINGS.highHdd, ...raw.highHdd },
|
||||||
tunnelHighPing: { ...DEFAULT_ALERT_SETTINGS.tunnelHighPing, ...raw.tunnelHighPing },
|
tunnelThresholds: Array.isArray(raw.tunnelThresholds) ? raw.tunnelThresholds : [],
|
||||||
tunnelLowSpeed: { ...DEFAULT_ALERT_SETTINGS.tunnelLowSpeed, ...raw.tunnelLowSpeed },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,13 +190,15 @@ async function getAlerts(req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3) Туннели (карта сети): высокий пинг и/или низкая скорость
|
// 3) Туннели (карта сети): индивидуальные пороги по пингу и скорости
|
||||||
const tunnelPingSettings = settings.tunnelHighPing || {};
|
const rawTunnelThresholds = Array.isArray(settings.tunnelThresholds)
|
||||||
const tunnelSpeedSettings = settings.tunnelLowSpeed || {};
|
? settings.tunnelThresholds
|
||||||
const tunnelPingEnabled = Boolean(tunnelPingSettings.enabled);
|
: [];
|
||||||
const tunnelSpeedEnabled = Boolean(tunnelSpeedSettings.enabled);
|
const tunnelThresholds = rawTunnelThresholds.filter(
|
||||||
|
(t) => t && (t.fromKey || t.toKey) && t.enabled !== false
|
||||||
|
);
|
||||||
|
|
||||||
if (tunnelPingEnabled || tunnelSpeedEnabled) {
|
if (tunnelThresholds.length > 0) {
|
||||||
let cache = null;
|
let cache = null;
|
||||||
let connectionsData = null;
|
let connectionsData = null;
|
||||||
try {
|
try {
|
||||||
@@ -221,88 +220,127 @@ async function getAlerts(req, res) {
|
|||||||
const pingMap = cache.pingMap && typeof cache.pingMap === 'object' ? cache.pingMap : {};
|
const pingMap = cache.pingMap && typeof cache.pingMap === 'object' ? cache.pingMap : {};
|
||||||
const speedMap = cache.speedMap && typeof cache.speedMap === 'object' ? cache.speedMap : {};
|
const speedMap = cache.speedMap && typeof cache.speedMap === 'object' ? cache.speedMap : {};
|
||||||
|
|
||||||
const thPingMs = Math.max(1, Number(tunnelPingSettings.thresholdMs) || 0);
|
const serversByIp = new Map();
|
||||||
const minDownMbps = Math.max(0, Number(tunnelSpeedSettings.minDownloadMbps) || 0);
|
servers.forEach((s) => {
|
||||||
const minUpMbps = Math.max(0, Number(tunnelSpeedSettings.minUploadMbps) || 0);
|
if (s && s.ip) {
|
||||||
|
serversByIp.set(String(s.ip), s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (thPingMs > 0 || minDownMbps > 0 || minUpMbps > 0) {
|
const thresholdsByKey = new Map();
|
||||||
const serversByIp = new Map();
|
const makeKey = (fromKey, toKey, iface) => {
|
||||||
servers.forEach((s) => {
|
const a = String(fromKey || '');
|
||||||
if (s && s.ip) {
|
const b = String(toKey || '');
|
||||||
serversByIp.set(String(s.ip), s);
|
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) => {
|
connectionsData.connections.forEach((c) => {
|
||||||
const fromIp = String(c.from);
|
const fromIp = String(c.from);
|
||||||
const toIp = String(c.to);
|
const toIp = String(c.to);
|
||||||
if (!fromIp || !toIp || fromIp === toIp) return;
|
if (!fromIp || !toIp || fromIp === toIp) return;
|
||||||
|
|
||||||
const fromServer = serversByIp.get(fromIp) || null;
|
const fromServer = serversByIp.get(fromIp) || null;
|
||||||
const toServer = serversByIp.get(toIp) || null;
|
const toServer = serversByIp.get(toIp) || null;
|
||||||
const fromName = fromServer?.name || fromServer?.dns || fromIp;
|
const fromName = fromServer?.name || fromServer?.dns || fromIp;
|
||||||
const toName = toServer?.name || toServer?.dns || toIp;
|
const toName = toServer?.name || toServer?.dns || toIp;
|
||||||
|
|
||||||
const pingMapKey = edgePingKey(fromIp, toIp);
|
const fromKey = c.fromKey || fromIp;
|
||||||
const pingMs = typeof pingMap[pingMapKey] === 'number' ? pingMap[pingMapKey] : null;
|
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) {
|
const maxPingMs = Math.max(0, Number(th.maxPingMs) || 0);
|
||||||
alerts.push({
|
const minDownMbps = Math.max(0, Number(th.minDownloadMbps) || 0);
|
||||||
id: `tunnel-ping-${pingMapKey}-${thPingMs}`,
|
const minUpMbps = Math.max(0, Number(th.minUploadMbps) || 0);
|
||||||
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 pingMapKey = edgePingKey(fromIp, toIp);
|
||||||
const sk = speedKey(c.fromKey, c.toKey);
|
const pingMs = typeof pingMap[pingMapKey] === 'number' ? pingMap[pingMapKey] : null;
|
||||||
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);
|
if (maxPingMs > 0 && pingMs != null && pingMs >= maxPingMs) {
|
||||||
const upTooLow = minUpMbps > 0 && (upMbps == null || upMbps < minUpMbps);
|
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) {
|
if ((minDownMbps > 0 || minUpMbps > 0) && c.fromKey && c.toKey) {
|
||||||
const parts = [];
|
const sk = speedKey(c.fromKey, c.toKey);
|
||||||
if (downTooLow) {
|
const speed = speedMap[sk];
|
||||||
parts.push(
|
if (speed && typeof speed === 'object') {
|
||||||
`↓ ${downMbps != null ? downMbps.toFixed(1) : '—'} Мбит/с (порог ${minDownMbps} Мбит/с)`
|
const downMbps =
|
||||||
);
|
typeof speed.tcpDownloadBps === 'number' ? speed.tcpDownloadBps / 1e6 : null;
|
||||||
}
|
const upMbps =
|
||||||
if (upTooLow) {
|
typeof speed.tcpUploadBps === 'number' ? speed.tcpUploadBps / 1e6 : null;
|
||||||
parts.push(
|
|
||||||
`↑ ${upMbps != null ? upMbps.toFixed(1) : '—'} Мбит/с (порог ${minUpMbps} Мбит/с)`
|
const downTooLow = minDownMbps > 0 && (downMbps == null || downMbps < minDownMbps);
|
||||||
);
|
const upTooLow = minUpMbps > 0 && (upMbps == null || upMbps < minUpMbps);
|
||||||
}
|
|
||||||
const ifaceLabel = c.interfaceName ? `, интерфейс ${c.interfaceName}` : '';
|
if (downTooLow || upTooLow) {
|
||||||
alerts.push({
|
const parts = [];
|
||||||
id: `tunnel-speed-${sk}-${minDownMbps}-${minUpMbps}`,
|
if (downTooLow) {
|
||||||
type: 'tunnel_low_speed',
|
parts.push(
|
||||||
severity: 'warning',
|
`↓ ${downMbps != null ? downMbps.toFixed(1) : '—'} Мбит/с (порог ${minDownMbps} Мбит/с)`
|
||||||
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 (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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+239
-123
@@ -114,12 +114,9 @@ export default function SettingsPage() {
|
|||||||
const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true);
|
const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true);
|
||||||
const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90');
|
const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90');
|
||||||
const [alertHighHddDurationMinutes, setAlertHighHddDurationMinutes] = useState('10');
|
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 [serversList, setServersList] = useState([]);
|
||||||
|
const [tunnelConnections, setTunnelConnections] = useState([]);
|
||||||
|
const [tunnelThresholds, setTunnelThresholds] = useState([]);
|
||||||
const [sidebarSearch, setSidebarSearch] = useState('');
|
const [sidebarSearch, setSidebarSearch] = useState('');
|
||||||
const [activeSection, setActiveSection] = useState(() => {
|
const [activeSection, setActiveSection] = useState(() => {
|
||||||
const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || '';
|
const hash = (typeof location.hash === 'string' && location.hash.slice(1)) || '';
|
||||||
@@ -234,9 +231,10 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const [settingsRes, serversRes] = await Promise.all([
|
const [settingsRes, serversRes, networkRes] = await Promise.all([
|
||||||
api.get('/ui-settings'),
|
api.get('/ui-settings'),
|
||||||
api.get('/servers').catch(() => ({ data: [] })),
|
api.get('/servers').catch(() => ({ data: [] })),
|
||||||
|
api.get('/network-config').catch(() => ({ data: null })),
|
||||||
]);
|
]);
|
||||||
const data = settingsRes?.data || {};
|
const data = settingsRes?.data || {};
|
||||||
setRawSettings(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 || {};
|
const a = data?.alertSettings || {};
|
||||||
setAlertServerOffline(a.serverOffline?.enabled !== false);
|
setAlertServerOffline(a.serverOffline?.enabled !== false);
|
||||||
setAlertServerOfflineMinutes(
|
setAlertServerOfflineMinutes(
|
||||||
@@ -356,23 +385,40 @@ export default function SettingsPage() {
|
|||||||
setAlertHighHddDurationMinutes(
|
setAlertHighHddDurationMinutes(
|
||||||
a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10'
|
a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10'
|
||||||
);
|
);
|
||||||
setAlertTunnelHighPingEnabled(a.tunnelHighPing?.enabled === true);
|
|
||||||
setAlertTunnelHighPingThresholdMs(
|
const rawTunnelThresholds = Array.isArray(a.tunnelThresholds) ? a.tunnelThresholds : [];
|
||||||
a.tunnelHighPing?.thresholdMs != null ? String(a.tunnelHighPing.thresholdMs) : '0'
|
setTunnelThresholds(
|
||||||
);
|
rawTunnelThresholds
|
||||||
setAlertTunnelLowSpeedEnabled(a.tunnelLowSpeed?.enabled === true);
|
.filter((t) => t && (t.fromKey || t.toKey))
|
||||||
setAlertTunnelLowSpeedMinDownload(
|
.map((t) => ({
|
||||||
a.tunnelLowSpeed?.minDownloadMbps != null
|
fromKey: String(t.fromKey || '').trim(),
|
||||||
? String(a.tunnelLowSpeed.minDownloadMbps)
|
toKey: String(t.toKey || '').trim(),
|
||||||
: '0'
|
interfaceName: t.interfaceName != null ? String(t.interfaceName) : '',
|
||||||
);
|
enabled: t.enabled !== false,
|
||||||
setAlertTunnelLowSpeedMinUpload(
|
maxPingMs:
|
||||||
a.tunnelLowSpeed?.minUploadMbps != null ? String(a.tunnelLowSpeed.minUploadMbps) : '0'
|
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 =
|
const e =
|
||||||
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
||||||
setEtag(e ? String(e) : '');
|
setEtag(e ? String(e) : '');
|
||||||
setServersList(Array.isArray(serversRes?.data) ? serversRes.data : []);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('Не удалось загрузить настройки');
|
setError('Не удалось загрузить настройки');
|
||||||
} finally {
|
} 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 () => {
|
const onSave = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
@@ -483,7 +569,7 @@ export default function SettingsPage() {
|
|||||||
interfaceName: p.interfaceName,
|
interfaceName: p.interfaceName,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
alertSettings: {
|
alertSettings: {
|
||||||
serverOffline: {
|
serverOffline: {
|
||||||
enabled: alertServerOffline,
|
enabled: alertServerOffline,
|
||||||
offlineMinutes: Math.max(
|
offlineMinutes: Math.max(
|
||||||
@@ -531,24 +617,28 @@ export default function SettingsPage() {
|
|||||||
Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)
|
Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
tunnelHighPing: {
|
tunnelThresholds: Array.isArray(tunnelThresholds)
|
||||||
enabled: alertTunnelHighPingEnabled,
|
? tunnelThresholds
|
||||||
thresholdMs: Math.max(
|
.filter((t) => {
|
||||||
1,
|
const fromKey = String(t.fromKey || '').trim();
|
||||||
Math.min(5000, parseInt(alertTunnelHighPingThresholdMs, 10) || 0)
|
const toKey = String(t.toKey || '').trim();
|
||||||
),
|
if (!fromKey || !toKey) return false;
|
||||||
},
|
if (t.enabled === false) return false;
|
||||||
tunnelLowSpeed: {
|
const maxPing = parseInt(t.maxPingMs, 10) || 0;
|
||||||
enabled: alertTunnelLowSpeedEnabled,
|
const minDown = parseInt(t.minDownloadMbps, 10) || 0;
|
||||||
minDownloadMbps: Math.max(
|
const minUp = parseInt(t.minUploadMbps, 10) || 0;
|
||||||
0,
|
return maxPing > 0 || minDown > 0 || minUp > 0;
|
||||||
Math.min(10000, parseInt(alertTunnelLowSpeedMinDownload, 10) || 0)
|
})
|
||||||
),
|
.map((t) => ({
|
||||||
minUploadMbps: Math.max(
|
fromKey: String(t.fromKey || '').trim(),
|
||||||
0,
|
toKey: String(t.toKey || '').trim(),
|
||||||
Math.min(10000, parseInt(alertTunnelLowSpeedMinUpload, 10) || 0)
|
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 };
|
const payload = { settings: mergedSettings, etag };
|
||||||
@@ -1399,94 +1489,120 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Пинг по туннелям (карта сети) */}
|
{/* Туннели (карта сети): индивидуальные пороги */}
|
||||||
<div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
|
<div className="list-group-item py-3">
|
||||||
<div className="flex-grow-1 min-w-0">
|
<div className="fw-semibold mb-1">Туннели (карта сети)</div>
|
||||||
<div className="fw-semibold">Пинг по туннелям</div>
|
<div className="text-muted small mb-2">
|
||||||
|
Для каждого туннеля можно задать собственные пороги по пингу и скорости. Используются данные
|
||||||
|
с планировщика карты сети и раздела «Сетевые настройки».
|
||||||
|
</div>
|
||||||
|
{tunnelConnections.length === 0 && (
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
Срабатывает, если средний пинг по туннелю на карте сети превышает указанный порог.
|
Нет настроенных туннелей. Добавьте связи в разделе «Сетевые настройки».
|
||||||
Применяется ко всем туннелям из раздела «Сетевые настройки».
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
{tunnelConnections.length > 0 && (
|
||||||
<div className="input-group input-group-sm" style={{ width: 110 }}>
|
<div className="table-responsive">
|
||||||
<input
|
<table className="table table-sm table-transparent mb-0 align-middle">
|
||||||
type="number"
|
<thead>
|
||||||
className="form-control"
|
<tr>
|
||||||
min={1}
|
<th style={{ width: '32%' }}>Туннель</th>
|
||||||
max={5000}
|
<th style={{ width: '16%' }}>Интерфейс</th>
|
||||||
value={alertTunnelHighPingThresholdMs}
|
<th style={{ width: '16%' }}>Макс. пинг (ms)</th>
|
||||||
onChange={(e) => setAlertTunnelHighPingThresholdMs(e.target.value)}
|
<th style={{ width: '18%' }}>Мин. ↓ (Мбит/с)</th>
|
||||||
disabled={saving || !alertTunnelHighPingEnabled}
|
<th style={{ width: '18%' }}>Мин. ↑ (Мбит/с)</th>
|
||||||
/>
|
<th style={{ width: '8%' }}>Вкл</th>
|
||||||
<span className="input-group-text">ms</span>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{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 (
|
||||||
|
<tr key={`${conn.fromKey}-${conn.toKey}-${conn.interfaceName || ''}`}>
|
||||||
|
<td>
|
||||||
|
<div className="small">
|
||||||
|
<span className="fw-semibold">
|
||||||
|
{conn.fromLabel} ⇄ {conn.toLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted small font-monospace">
|
||||||
|
{conn.from} ⇄ {conn.to}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="small">
|
||||||
|
{conn.interfaceName ? (
|
||||||
|
<span className="font-monospace">{conn.interfaceName}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
min={0}
|
||||||
|
max={5000}
|
||||||
|
value={maxPingMs}
|
||||||
|
onChange={(e) =>
|
||||||
|
upsertTunnelThreshold(conn, { maxPingMs: e.target.value })
|
||||||
|
}
|
||||||
|
disabled={saving || !enabled}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
min={0}
|
||||||
|
max={10000}
|
||||||
|
value={minDown}
|
||||||
|
onChange={(e) =>
|
||||||
|
upsertTunnelThreshold(conn, {
|
||||||
|
minDownloadMbps: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={saving || !enabled}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
min={0}
|
||||||
|
max={10000}
|
||||||
|
value={minUp}
|
||||||
|
onChange={(e) =>
|
||||||
|
upsertTunnelThreshold(conn, {
|
||||||
|
minUploadMbps: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={saving || !enabled}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="form-check form-switch mb-0 d-inline-flex">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
upsertTunnelThreshold(conn, { enabled: e.target.checked })
|
||||||
|
}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-check form-switch mb-0">
|
)}
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="alertTunnelHighPingEnabled"
|
|
||||||
checked={alertTunnelHighPingEnabled}
|
|
||||||
onChange={(e) => setAlertTunnelHighPingEnabled(e.target.checked)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label small" htmlFor="alertTunnelHighPingEnabled">
|
|
||||||
Вкл
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Скорость по туннелям (карта сети) */}
|
|
||||||
<div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
|
|
||||||
<div className="flex-grow-1 min-w-0">
|
|
||||||
<div className="fw-semibold">Скорость по туннелям</div>
|
|
||||||
<div className="text-muted small">
|
|
||||||
Срабатывает, если измеренная скорость по туннелю (download и/или upload) ниже указанных
|
|
||||||
порогов. Используются последние результаты планировщика карты сети.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
|
||||||
<div className="input-group input-group-sm" style={{ width: 130 }}>
|
|
||||||
<span className="input-group-text">↓</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
min={0}
|
|
||||||
max={10000}
|
|
||||||
value={alertTunnelLowSpeedMinDownload}
|
|
||||||
onChange={(e) => setAlertTunnelLowSpeedMinDownload(e.target.value)}
|
|
||||||
disabled={saving || !alertTunnelLowSpeedEnabled}
|
|
||||||
/>
|
|
||||||
<span className="input-group-text">Мбит/с</span>
|
|
||||||
</div>
|
|
||||||
<div className="input-group input-group-sm" style={{ width: 130 }}>
|
|
||||||
<span className="input-group-text">↑</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
min={0}
|
|
||||||
max={10000}
|
|
||||||
value={alertTunnelLowSpeedMinUpload}
|
|
||||||
onChange={(e) => setAlertTunnelLowSpeedMinUpload(e.target.value)}
|
|
||||||
disabled={saving || !alertTunnelLowSpeedEnabled}
|
|
||||||
/>
|
|
||||||
<span className="input-group-text">Мбит/с</span>
|
|
||||||
</div>
|
|
||||||
<div className="form-check form-switch mb-0">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
id="alertTunnelLowSpeedEnabled"
|
|
||||||
checked={alertTunnelLowSpeedEnabled}
|
|
||||||
onChange={(e) => setAlertTunnelLowSpeedEnabled(e.target.checked)}
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label small" htmlFor="alertTunnelLowSpeedEnabled">
|
|
||||||
Вкл
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user