From 6a90f4079d111837fe0a2c808712d753c0334099 Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 11 Feb 2026 11:48:32 +0700 Subject: [PATCH] feat(ping): add ping caching functionality with configurable duration in UI settings --- backend/routes/mikrotikConfigRoutes.js | 64 ++++++++++++++++++----- frontend/src/components/SettingsModal.jsx | 15 ++++++ 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index bd958f7..889b86b 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -3,8 +3,9 @@ * Использует RouterOS REST API — требует RouterOS 7.1+ с www-ssl (443) или www (80) */ +const crypto = require('crypto'); const { GetObjectCommand } = require('@aws-sdk/client-s3'); -const { s3, BUCKET_NAME, streamToString, readS3TextObject } = require('../services/s3Service'); +const { s3, BUCKET_NAME, streamToString, readS3TextObject, writeS3JsonObject } = require('../services/s3Service'); const { sendError, sendOk } = require('../middleware/errorHandler'); const { decrypt } = require('../utils/encryption'); const { readServersFromS3 } = require('./serversRoutes'); @@ -19,8 +20,9 @@ const { createRosClient, applyBlock } = require('../services/mikrotikApplyServic const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json'; const NETWORK_CONFIG_KEY = 'network-config.json'; const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json'; +const PING_CACHE_PREFIX = 'ping-cache/'; -/** Загрузить UI-настройки из S3 (для pingDomain и др.) */ +/** Загрузить UI-настройки из S3 (для pingDomain, pingCacheMinutes и др.) */ async function loadUiSettings() { try { const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' })); @@ -31,6 +33,12 @@ async function loadUiSettings() { } } +/** Ключ кеша пинга по serverId, gatewayIp, target */ +function pingCacheKey(serverId, gatewayIp, target) { + const payload = `${serverId}|${gatewayIp || ''}|${target || ''}`; + return PING_CACHE_PREFIX + crypto.createHash('sha256').update(payload, 'utf8').digest('hex') + '.json'; +} + /** Загрузить network-config из S3 */ async function loadNetworkConfig() { try { @@ -311,15 +319,41 @@ async function pingViaInterface(req, res) { return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST'); } - // Цель пинга: из тела запроса, иначе из настроек интерфейса (Домен для пинга), иначе по умолчанию + const uiSettings = await loadUiSettings(); + + // Цель пинга: из тела запроса, иначе из настроек (Домен для пинга), иначе по умолчанию if (target == null || String(target).trim() === '') { - const uiSettings = await loadUiSettings(); const pingDomain = (uiSettings.pingDomain && String(uiSettings.pingDomain).trim()) || ''; target = pingDomain || 'www.gstatic.com'; } else { target = String(target).trim(); } + const pingCacheMinutes = Math.max(0, parseInt(uiSettings.pingCacheMinutes, 10) || 0); + const cacheKey = pingCacheMinutes > 0 ? pingCacheKey(serverId, gatewayIp, target) : null; + + if (cacheKey) { + try { + const data = await readS3TextObject(cacheKey).catch(() => null); + if (data?.body) { + const cached = JSON.parse(data.body); + const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0; + const ttlMs = pingCacheMinutes * 60 * 1000; + if (cachedAt && Date.now() - cachedAt < ttlMs) { + return res.json({ + ok: true, + avgMs: cached.avgMs, + minMs: cached.minMs, + maxMs: cached.maxMs, + loss: cached.loss, + }); + } + } + } catch (_) { + // кеш протух или невалиден — выполняем реальный пинг + } + } + const servers = await readServersFromS3(); const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId); if (!server || server.type !== 'jumphost') { @@ -486,15 +520,19 @@ async function pingViaInterface(req, res) { ? Number(String(summary['packet-loss']).replace('%', '')) : null; - // ВАЖНО: возвращаем данные напрямую, без sendOk, - // чтобы фронтенд (EasySwitch) мог читать поля avgMs/minMs/maxMs/loss - return res.json({ - ok: true, - avgMs, - minMs, - maxMs, - loss, - }); + const result = { ok: true, avgMs, minMs, maxMs, loss }; + + if (cacheKey) { + writeS3JsonObject(cacheKey, { + avgMs, + minMs, + maxMs, + loss, + cachedAt: Date.now(), + }).catch((err) => console.warn('[mikrotik][pingViaInterface] cache write failed:', err?.message)); + } + + return res.json(result); } catch (error) { const msg = error.response?.data?.message || error.message || 'Ping failed'; const status = error.response?.status; diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index 8ce7715..e183cee 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -19,6 +19,7 @@ export default function SettingsModal({ open, onClose }) { const [wsUrl, setWsUrl] = useState(''); const [baseAS, setBaseAS] = useState('65001'); const [pingDomain, setPingDomain] = useState(''); + const [pingCacheMinutes, setPingCacheMinutes] = useState(''); const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState(''); const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState(''); @@ -37,6 +38,7 @@ export default function SettingsModal({ open, onClose }) { setWsUrl(String(data?.wsUpdateUrl || '')); setBaseAS(String(data?.baseAS || '65001')); setPingDomain(String(data?.pingDomain || '').trim()); + setPingCacheMinutes(data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : ''); setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || '')); setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || '')); const e = res?.headers?.etag || res?.headers?.ETag || ''; @@ -100,6 +102,7 @@ export default function SettingsModal({ open, onClose }) { wsUpdateUrl: String(wsUrl || '').trim(), baseAS: String(baseAS || '65001').trim(), pingDomain: String(pingDomain || '').trim(), + pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0), ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(), ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(), }; @@ -212,6 +215,18 @@ export default function SettingsModal({ open, onClose }) { disabled={loading || saving} /> + +
Настройка PTR зоны