feat(NetworkMapDashboard, SettingsPage): implement ping cache duration settings for network map and enhance ping request handling
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s

This commit is contained in:
2026-02-17 17:08:57 +07:00
parent 48ee2a962e
commit 965eb7467b
2 changed files with 64 additions and 18 deletions
+41 -18
View File
@@ -29,6 +29,8 @@ export default function NetworkMapDashboard() {
const [servers, setServers] = useState([]);
const [connections, setConnections] = useState([]);
const [pingMap, setPingMap] = useState({});
const pingTimestampsRef = useRef({});
const [pingCacheSeconds, setPingCacheSeconds] = useState(0);
const [loading, setLoading] = useState(true);
const [pingLoading, setPingLoading] = useState(false);
const [error, setError] = useState(null);
@@ -39,13 +41,20 @@ export default function NetworkMapDashboard() {
setLoading(true);
setError(null);
try {
const [serversRes, configRes] = await Promise.all([
const [serversRes, configRes, uiSettingsRes] = await Promise.all([
api.get('/servers'),
api.get('/network-config').catch(() => ({ data: null })),
api.get('/ui-settings').catch(() => ({ data: {} })),
]);
const serversList = Array.isArray(serversRes?.data) ? serversRes.data : [];
const config = configRes?.data || {};
const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
const uiSettings = uiSettingsRes?.data || {};
const ttlSeconds = Math.max(
0,
parseInt(uiSettings.networkMapPingCacheSeconds, 10) || 0
);
setPingCacheSeconds(ttlSeconds);
const getServer = (serverId) =>
serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId);
@@ -88,7 +97,7 @@ export default function NetworkMapDashboard() {
pingAbortRef.current = false;
setPingLoading(true);
const key = (a, b) => `${a}:${b}`;
const newMap = {};
const ttlMs = Math.max(0, (pingCacheSeconds || 0) * 1000);
const tasks = [];
connections.forEach((c) => {
@@ -102,24 +111,42 @@ export default function NetworkMapDashboard() {
const canPingTo =
srcTo && ['jumphost', 'home'].includes(String(srcTo.type || '').toLowerCase()) && c.internalToFrom;
// Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config:
// server1 -> remoteIp, server2 -> localIp. serverId — ключ (id/dns/ip) как в servers.json.
// Проверка кеша: если TTL не истёк, не запускаем новый запрос
const keyFromTo = key(from, to);
const keyToFrom = key(to, from);
const now = Date.now();
if (ttlMs > 0) {
const tsFrom = pingTimestampsRef.current[keyFromTo];
if (tsFrom && now - tsFrom < ttlMs) {
// уже есть актуальное значение для from->to
} else if (!canPingFrom) {
// если пинговать нельзя, но кеша нет — оставляем как есть
}
const tsTo = pingTimestampsRef.current[keyToFrom];
if (tsTo && now - tsTo < ttlMs) {
// уже есть актуальное значение для to->from
} else if (!canPingTo) {
// нельзя пинговать и кеша нет — пропускаем
}
}
// Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config
if (canPingFrom) {
tasks.push(async () => {
if (pingAbortRef.current) return;
try {
const { data } = await api.post('/mikrotik/ping', {
serverId: c.fromKey,
// ВАЖНО: передаём внутренний IP и как target, и как gatewayIp,
// чтобы backend мог по gatewayIp найти нужный tunnel interface.
target: c.internalFromTo,
gatewayIp: c.internalFromTo,
count: 3,
});
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
return { key: key(from, to), ms };
pingTimestampsRef.current[keyFromTo] = Date.now();
setPingMap((prev) => ({ ...prev, [keyFromTo]: ms }));
} catch {
return { key: key(from, to), ms: null };
pingTimestampsRef.current[keyFromTo] = Date.now();
setPingMap((prev) => ({ ...prev, [keyFromTo]: null }));
}
});
}
@@ -130,29 +157,25 @@ export default function NetworkMapDashboard() {
try {
const { data } = await api.post('/mikrotik/ping', {
serverId: c.toKey,
// Аналогично для обратного направления
target: c.internalToFrom,
gatewayIp: c.internalToFrom,
count: 3,
});
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
return { key: key(to, from), ms };
pingTimestampsRef.current[keyToFrom] = Date.now();
setPingMap((prev) => ({ ...prev, [keyToFrom]: ms }));
} catch {
return { key: key(to, from), ms: null };
pingTimestampsRef.current[keyToFrom] = Date.now();
setPingMap((prev) => ({ ...prev, [keyToFrom]: null }));
}
});
}
});
const results = await runWithLimit(tasks, 4);
await runWithLimit(tasks, 4);
if (pingAbortRef.current) return;
results.forEach((r) => {
const v = r?.value;
if (v && v.key != null) newMap[v.key] = v.ms ?? null;
});
setPingMap((prev) => ({ ...prev, ...newMap }));
setPingLoading(false);
}, [connections]);
}, [connections, servers, pingCacheSeconds]);
useEffect(() => {
fetchData();
+23
View File
@@ -68,6 +68,7 @@ export default function SettingsPage() {
const [baseAS, setBaseAS] = useState('65001');
const [pingDomain, setPingDomain] = useState('');
const [pingCacheMinutes, setPingCacheMinutes] = useState('');
const [networkMapPingCacheSeconds, setNetworkMapPingCacheSeconds] = useState('');
const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState('');
const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState('');
const [pingServicesSource, setPingServicesSource] = useState('web');
@@ -206,6 +207,11 @@ export default function SettingsPage() {
setPingCacheMinutes(
data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : ''
);
setNetworkMapPingCacheSeconds(
data?.networkMapPingCacheSeconds != null
? String(data.networkMapPingCacheSeconds)
: ''
);
setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || ''));
setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || ''));
setPingServicesSource(
@@ -300,6 +306,10 @@ export default function SettingsPage() {
baseAS: String(baseAS || '65001').trim(),
pingDomain: String(pingDomain || '').trim(),
pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0),
networkMapPingCacheSeconds: Math.max(
0,
parseInt(networkMapPingCacheSeconds, 10) || 0
),
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
pingServicesSource:
@@ -528,6 +538,19 @@ export default function SettingsPage() {
min={0}
/>
</div>
<div className="col-md-6">
<FormField
label="Кеш пингов на карте сети (сек)"
name="networkMapPingCacheSeconds"
type="number"
value={networkMapPingCacheSeconds}
onChange={setNetworkMapPingCacheSeconds}
placeholder="0"
helpText="0 — без кеша. Иначе пинги между серверами на карте сети кешируются на указанный срок."
disabled={saving}
min={0}
/>
</div>
</div>
</>
)}