feat(network-map): add network speed thresholds for alerts and enhance settings UI for configuration
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m32s

This commit is contained in:
2026-02-25 02:15:10 +07:00
parent d53100f5bd
commit d68b3aa104
4 changed files with 116 additions and 3 deletions
+12
View File
@@ -18,6 +18,12 @@ 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 },
/** Глобальные пороги карты сети для UI-аналитики */
networkMapNodeThresholds: {
lowSpeedMbps: 40,
belowNormSpeedMbps: 80,
normalSpeedMbps: 120,
},
/** Индивидуальные пороги по туннелям (карта сети) */
tunnelThresholds: [],
};
@@ -30,6 +36,12 @@ function getAlertSettings(uiSettings) {
highCpu: { ...DEFAULT_ALERT_SETTINGS.highCpu, ...raw.highCpu },
highRam: { ...DEFAULT_ALERT_SETTINGS.highRam, ...raw.highRam },
highHdd: { ...DEFAULT_ALERT_SETTINGS.highHdd, ...raw.highHdd },
networkMapNodeThresholds: {
...DEFAULT_ALERT_SETTINGS.networkMapNodeThresholds,
...(raw.networkMapNodeThresholds && typeof raw.networkMapNodeThresholds === 'object'
? raw.networkMapNodeThresholds
: {}),
},
tunnelThresholds: Array.isArray(raw.tunnelThresholds) ? raw.tunnelThresholds : [],
};
}
+7
View File
@@ -76,6 +76,7 @@ export default function NetworkMapDashboard() {
const lastSuccessfulPingRef = useRef({});
const lastSuccessfulSpeedRef = useRef({});
const [pingCacheSeconds, setPingCacheSeconds] = useState(0);
const [alertSettings, setAlertSettings] = useState({});
const [loading, setLoading] = useState(true);
const [pingLoading, setPingLoading] = useState(false);
const [speedLoading, setSpeedLoading] = useState(false);
@@ -102,12 +103,17 @@ export default function NetworkMapDashboard() {
const config = configRes?.data || {};
const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
const uiSettings = uiSettingsRes?.data || {};
const uiAlertSettings =
uiSettings.alertSettings && typeof uiSettings.alertSettings === 'object'
? uiSettings.alertSettings
: {};
const cache = cacheRes?.data || {};
const ttlSeconds = Math.max(
0,
parseInt(uiSettings.networkMapPingCacheSeconds, 10) || 0
);
setPingCacheSeconds(ttlSeconds);
setAlertSettings(uiAlertSettings);
const getServer = (serverId) =>
serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId);
@@ -462,6 +468,7 @@ export default function NetworkMapDashboard() {
onRefreshSpeedForConnection={handleRefreshSpeedForConnection}
pingLoading={pingLoading}
speedLoading={speedLoading}
alertSettings={alertSettings}
/>
)}
</div>
+19 -3
View File
@@ -144,6 +144,7 @@ export default function NetworkMapUnifi({
onRefreshSpeedForConnection,
pingLoading = false,
speedLoading = false,
alertSettings = {},
}) {
const containerRef = useRef(null);
const svgRef = useRef(null);
@@ -154,6 +155,18 @@ export default function NetworkMapUnifi({
const [hoveredEdgeKey, setHoveredEdgeKey] = useState(null);
const [selectedEdge, setSelectedEdge] = useState(null);
const mapSpeedThresholds = useMemo(() => {
const raw =
alertSettings?.networkMapNodeThresholds &&
typeof alertSettings.networkMapNodeThresholds === 'object'
? alertSettings.networkMapNodeThresholds
: {};
const low = Math.max(1, Number(raw.lowSpeedMbps) || 40);
const belowNorm = Math.max(low, Number(raw.belowNormSpeedMbps) || 80);
const normal = Math.max(belowNorm, Number(raw.normalSpeedMbps) || 120);
return { low, belowNorm, normal };
}, [alertSettings]);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
@@ -532,12 +545,15 @@ export default function NetworkMapUnifi({
}
if (n.minSpeedMbps != null) {
if (n.minSpeedMbps < 40) {
if (n.minSpeedMbps < mapSpeedThresholds.low) {
issues.push('Низкая скорость');
riskScore += 3;
} else if (n.minSpeedMbps < 80) {
} else if (n.minSpeedMbps < mapSpeedThresholds.belowNorm) {
issues.push('Скорость ниже нормы');
riskScore += 2;
} else if (n.minSpeedMbps < mapSpeedThresholds.normal) {
issues.push('Норма скорости не достигнута');
riskScore += 1;
}
}
@@ -564,7 +580,7 @@ export default function NetworkMapUnifi({
(b.maxPingMs ?? -1) - (a.maxPingMs ?? -1) ||
(a.minSpeedMbps ?? Infinity) - (b.minSpeedMbps ?? Infinity)
);
}, [servers, connections, routedEdges]);
}, [servers, connections, routedEdges, mapSpeedThresholds]);
const criticalNodes = useMemo(
() => problematicNodes.filter((n) => n.riskScore >= 4).slice(0, 8),
+78
View File
@@ -114,6 +114,9 @@ export default function SettingsPage() {
const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true);
const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90');
const [alertHighHddDurationMinutes, setAlertHighHddDurationMinutes] = useState('10');
const [alertMapLowSpeedMbps, setAlertMapLowSpeedMbps] = useState('40');
const [alertMapBelowNormSpeedMbps, setAlertMapBelowNormSpeedMbps] = useState('80');
const [alertMapNormalSpeedMbps, setAlertMapNormalSpeedMbps] = useState('120');
const [serversList, setServersList] = useState([]);
const [tunnelConnections, setTunnelConnections] = useState([]);
const [tunnelThresholds, setTunnelThresholds] = useState([]);
@@ -385,6 +388,21 @@ export default function SettingsPage() {
setAlertHighHddDurationMinutes(
a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10'
);
setAlertMapLowSpeedMbps(
a.networkMapNodeThresholds?.lowSpeedMbps != null
? String(a.networkMapNodeThresholds.lowSpeedMbps)
: '40'
);
setAlertMapBelowNormSpeedMbps(
a.networkMapNodeThresholds?.belowNormSpeedMbps != null
? String(a.networkMapNodeThresholds.belowNormSpeedMbps)
: '80'
);
setAlertMapNormalSpeedMbps(
a.networkMapNodeThresholds?.normalSpeedMbps != null
? String(a.networkMapNodeThresholds.normalSpeedMbps)
: '120'
);
const rawTunnelThresholds = Array.isArray(a.tunnelThresholds) ? a.tunnelThresholds : [];
setTunnelThresholds(
@@ -510,6 +528,9 @@ export default function SettingsPage() {
setSaving(true);
try {
const parsedLow = Math.max(1, parseInt(alertMapLowSpeedMbps, 10) || 40);
const parsedBelowNorm = Math.max(parsedLow, parseInt(alertMapBelowNormSpeedMbps, 10) || 80);
const parsedNormal = Math.max(parsedBelowNorm, parseInt(alertMapNormalSpeedMbps, 10) || 120);
const mergedSettings = {
...rawSettings,
dohServer: String(dohServer || '').trim(),
@@ -617,6 +638,11 @@ export default function SettingsPage() {
Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)
),
},
networkMapNodeThresholds: {
lowSpeedMbps: Math.min(100000, parsedLow),
belowNormSpeedMbps: Math.min(100000, parsedBelowNorm),
normalSpeedMbps: Math.min(100000, parsedNormal),
},
tunnelThresholds: Array.isArray(tunnelThresholds)
? tunnelThresholds
.filter((t) => {
@@ -1489,6 +1515,58 @@ export default function SettingsPage() {
</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 d-flex align-items-center gap-2">
<IconNetwork size={18} className="text-azure" />
Карта сети: пороги скорости
</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: 132 }}>
<span className="input-group-text">Низкая</span>
<input
type="number"
className="form-control"
min={1}
max={100000}
value={alertMapLowSpeedMbps}
onChange={(e) => setAlertMapLowSpeedMbps(e.target.value)}
disabled={saving}
/>
</div>
<div className="input-group input-group-sm" style={{ width: 162 }}>
<span className="input-group-text">Ниже нормы</span>
<input
type="number"
className="form-control"
min={1}
max={100000}
value={alertMapBelowNormSpeedMbps}
onChange={(e) => setAlertMapBelowNormSpeedMbps(e.target.value)}
disabled={saving}
/>
</div>
<div className="input-group input-group-sm" style={{ width: 120 }}>
<span className="input-group-text">Норма</span>
<input
type="number"
className="form-control"
min={1}
max={100000}
value={alertMapNormalSpeedMbps}
onChange={(e) => setAlertMapNormalSpeedMbps(e.target.value)}
disabled={saving}
/>
<span className="input-group-text">Мбит/с</span>
</div>
</div>
</div>
{/* Туннели (карта сети): индивидуальные пороги */}
<div className="list-group-item py-3">
<div className="fw-semibold mb-1">Туннели (карта сети)</div>