feat(alerts): add tunnel high ping and low speed alert settings with corresponding UI controls
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
This commit is contained in:
@@ -8,6 +8,8 @@ const { readServersFromS3 } = require('./serversRoutes');
|
|||||||
const { checkOneServerFast, loadUiSettingsSync } = require('./miscRoutes');
|
const { checkOneServerFast, loadUiSettingsSync } = require('./miscRoutes');
|
||||||
const { getResourceStatsData } = require('./resourceStatsRoutes');
|
const { getResourceStatsData } = require('./resourceStatsRoutes');
|
||||||
const { getUptimeCacheData } = require('./mikrotikConfigRoutes');
|
const { getUptimeCacheData } = require('./mikrotikConfigRoutes');
|
||||||
|
const { buildConnections } = require('../services/networkMapScheduler');
|
||||||
|
const { readS3TextObject } = require('../services/s3Service');
|
||||||
|
|
||||||
/** Значения по умолчанию для настроек оповещений. */
|
/** Значения по умолчанию для настроек оповещений. */
|
||||||
const DEFAULT_ALERT_SETTINGS = {
|
const DEFAULT_ALERT_SETTINGS = {
|
||||||
@@ -16,6 +18,10 @@ 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 },
|
||||||
|
/** Пороги по скорости туннелей (карта сети) */
|
||||||
|
tunnelLowSpeed: { enabled: false, minDownloadMbps: 0, minUploadMbps: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAlertSettings(uiSettings) {
|
function getAlertSettings(uiSettings) {
|
||||||
@@ -26,9 +32,19 @@ 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 },
|
||||||
|
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
|
* GET /api/alerts
|
||||||
* Возвращает список активных оповещений (учёт настроек из ui-settings.alertSettings).
|
* Возвращает список активных оповещений (учёт настроек из 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({
|
return res.json({
|
||||||
alerts,
|
alerts,
|
||||||
total: alerts.length,
|
total: alerts.length,
|
||||||
|
|||||||
+185
-17
@@ -114,6 +114,11 @@ 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 [sidebarSearch, setSidebarSearch] = useState('');
|
const [sidebarSearch, setSidebarSearch] = useState('');
|
||||||
const [activeSection, setActiveSection] = useState(() => {
|
const [activeSection, setActiveSection] = useState(() => {
|
||||||
@@ -321,18 +326,49 @@ export default function SettingsPage() {
|
|||||||
);
|
);
|
||||||
const a = data?.alertSettings || {};
|
const a = data?.alertSettings || {};
|
||||||
setAlertServerOffline(a.serverOffline?.enabled !== false);
|
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);
|
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);
|
setAlertHighCpuEnabled(a.highCpu?.enabled !== false);
|
||||||
setAlertHighCpuThreshold(a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85');
|
setAlertHighCpuThreshold(
|
||||||
setAlertHighCpuDurationMinutes(a.highCpu?.durationMinutes != null ? String(a.highCpu.durationMinutes) : '10');
|
a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85'
|
||||||
|
);
|
||||||
|
setAlertHighCpuDurationMinutes(
|
||||||
|
a.highCpu?.durationMinutes != null ? String(a.highCpu.durationMinutes) : '10'
|
||||||
|
);
|
||||||
setAlertHighRamEnabled(a.highRam?.enabled !== false);
|
setAlertHighRamEnabled(a.highRam?.enabled !== false);
|
||||||
setAlertHighRamThreshold(a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85');
|
setAlertHighRamThreshold(
|
||||||
setAlertHighRamDurationMinutes(a.highRam?.durationMinutes != null ? String(a.highRam.durationMinutes) : '10');
|
a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85'
|
||||||
|
);
|
||||||
|
setAlertHighRamDurationMinutes(
|
||||||
|
a.highRam?.durationMinutes != null ? String(a.highRam.durationMinutes) : '10'
|
||||||
|
);
|
||||||
setAlertHighHddEnabled(a.highHdd?.enabled !== false);
|
setAlertHighHddEnabled(a.highHdd?.enabled !== false);
|
||||||
setAlertHighHddThreshold(a.highHdd?.thresholdPercent != null ? String(a.highHdd.thresholdPercent) : '90');
|
setAlertHighHddThreshold(
|
||||||
setAlertHighHddDurationMinutes(a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10');
|
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 =
|
const e =
|
||||||
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
|
||||||
setEtag(e ? String(e) : '');
|
setEtag(e ? String(e) : '');
|
||||||
@@ -447,29 +483,71 @@ export default function SettingsPage() {
|
|||||||
interfaceName: p.interfaceName,
|
interfaceName: p.interfaceName,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
alertSettings: {
|
alertSettings: {
|
||||||
serverOffline: {
|
serverOffline: {
|
||||||
enabled: alertServerOffline,
|
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: {
|
mikrotikUnreachable: {
|
||||||
enabled: alertMikrotikUnreachable,
|
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: {
|
highCpu: {
|
||||||
enabled: alertHighCpuEnabled,
|
enabled: alertHighCpuEnabled,
|
||||||
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85)),
|
thresholdPercent: Math.max(
|
||||||
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighCpuDurationMinutes, 10) || 10)),
|
1,
|
||||||
|
Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85)
|
||||||
|
),
|
||||||
|
durationMinutes: Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(1440, parseInt(alertHighCpuDurationMinutes, 10) || 10)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
highRam: {
|
highRam: {
|
||||||
enabled: alertHighRamEnabled,
|
enabled: alertHighRamEnabled,
|
||||||
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighRamThreshold, 10) || 85)),
|
thresholdPercent: Math.max(
|
||||||
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighRamDurationMinutes, 10) || 10)),
|
1,
|
||||||
|
Math.min(100, parseInt(alertHighRamThreshold, 10) || 85)
|
||||||
|
),
|
||||||
|
durationMinutes: Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(1440, parseInt(alertHighRamDurationMinutes, 10) || 10)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
highHdd: {
|
highHdd: {
|
||||||
enabled: alertHighHddEnabled,
|
enabled: alertHighHddEnabled,
|
||||||
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighHddThreshold, 10) || 90)),
|
thresholdPercent: Math.max(
|
||||||
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)),
|
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() {
|
|||||||
</div>
|
</div>
|
||||||
</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="flex-grow-1 min-w-0">
|
||||||
|
<div className="fw-semibold">Пинг по туннелям</div>
|
||||||
|
<div className="text-muted small">
|
||||||
|
Срабатывает, если средний пинг по туннелю на карте сети превышает указанный порог.
|
||||||
|
Применяется ко всем туннелям из раздела «Сетевые настройки».
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<div className="input-group input-group-sm" style={{ width: 110 }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
min={1}
|
||||||
|
max={5000}
|
||||||
|
value={alertTunnelHighPingThresholdMs}
|
||||||
|
onChange={(e) => setAlertTunnelHighPingThresholdMs(e.target.value)}
|
||||||
|
disabled={saving || !alertTunnelHighPingEnabled}
|
||||||
|
/>
|
||||||
|
<span className="input-group-text">ms</span>
|
||||||
|
</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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user