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

This commit is contained in:
2026-02-24 22:23:12 +07:00
parent 53be4cd4ad
commit 80de7160b9
2 changed files with 359 additions and 205 deletions
+120 -82
View File
@@ -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
View File
@@ -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>
</> </>