diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index 1cc12a7..d7f1bb0 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -22,6 +22,7 @@ const NETWORK_CONFIG_KEY = 'network-config.json'; const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json'; const PING_CACHE_PREFIX = 'ping-cache/'; const SPEEDTEST_CACHE_PREFIX = 'speed-test-cache/'; +const UPTIME_CACHE_KEY = 'uptime-monitor-cache/latest.json'; /** Загрузить UI-настройки из S3 (для pingDomain, pingCacheMinutes и др.) */ async function loadUiSettings() { @@ -244,11 +245,64 @@ async function testMikrotikConnection(req, res) { } } +/** Записать результат проверки в кеш Uptime Monitor (S3). Не блокирует ответ. */ +function updateUptimeCache(serverId, entry) { + const { ok, lastCheckTs, ms } = entry; + return readS3TextObject(UPTIME_CACHE_KEY) + .catch(() => ({ body: '{}' })) + .then((data) => { + let cache = { results: {}, updatedAt: null }; + try { + const parsed = JSON.parse(data?.body || '{}'); + if (parsed && typeof parsed === 'object' && parsed.results) cache = parsed; + } catch (_) {} + cache.results[serverId] = { ok, lastCheckTs, ms }; + cache.updatedAt = Date.now(); + return writeS3JsonObject(UPTIME_CACHE_KEY, cache); + }) + .catch((e) => console.warn('[uptime] cache write failed:', e?.message)); +} + +/** + * GET /api/uptime/cache + * Возвращает закешированные результаты проверок, если кеш младше TTL из настроек. + */ +async function getUptimeCache(req, res) { + try { + const uiSettings = await loadUiSettings(); + const ttlSec = Math.max(0, parseInt(uiSettings.uptimeMonitorCacheSeconds, 10) || 120); + if (ttlSec === 0) { + return res.json({ results: {}, updatedAt: null }); + } + const data = await readS3TextObject(UPTIME_CACHE_KEY).catch(() => null); + if (!data?.body) { + return res.json({ results: {}, updatedAt: null }); + } + let cache = { results: {}, updatedAt: null }; + try { + const parsed = JSON.parse(data.body); + if (parsed && typeof parsed === 'object') cache = parsed; + } catch (_) {} + const now = Date.now(); + if (!cache.updatedAt || now - cache.updatedAt > ttlSec * 1000) { + return res.json({ results: {}, updatedAt: null }); + } + return res.json({ + results: cache.results && typeof cache.results === 'object' ? cache.results : {}, + updatedAt: cache.updatedAt, + }); + } catch (e) { + console.error('[uptime] getUptimeCache', e); + return res.json({ results: {}, updatedAt: null }); + } +} + /** * POST /api/uptime/check * Проверка доступности одного сервера (jumphost/home). Тип проверки берётся из ui-settings.uptimeMonitorCheckType. * Body: { serverId } * Returns: { ok: boolean, ms?: number } + * Результат пишется в кеш (S3) для отображения при заходе на страницу. */ async function uptimeCheck(req, res) { const t0 = Date.now(); @@ -280,6 +334,8 @@ async function uptimeCheck(req, res) { await client.print('system/resource'); ok = true; ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok, lastCheckTs, ms }); return res.json({ ok, ms }); } @@ -293,6 +349,8 @@ async function uptimeCheck(req, res) { ); if (!iface || (!iface.remoteIp && !iface.localIp)) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: 'Нет туннеля с внутренним адресом для этого сервера' }); } const target = iface.serverId === serverId || iface.serverId === server.ip || iface.serverId === server.dns @@ -301,15 +359,21 @@ async function uptimeCheck(req, res) { const gatewayIp = target; if (!target) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: 'Нет целевого адреса для пинга' }); } try { const result = await runPingViaRouter(serverId, gatewayIp, target, 3); ms = Date.now() - t0; ok = typeof result.avgMs === 'number'; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok, lastCheckTs, ms: ok ? result.avgMs : ms }); return res.json({ ok, ms: ok ? result.avgMs : ms }); } catch (err) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: err?.message || String(err) }); } } @@ -324,11 +388,15 @@ async function uptimeCheck(req, res) { const sourceServer = others[0]; if (!sourceServer) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: 'Нет другого jumphost для внешнего пинга' }); } const targetIp = server.ip || server.extIp; if (!targetIp) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: 'У сервера нет внешнего IP' }); } const sourceId = sourceServer.id || sourceServer.dns || sourceServer.ip; @@ -336,14 +404,20 @@ async function uptimeCheck(req, res) { const result = await runPingViaRouter(sourceId, null, targetIp, 3); ms = Date.now() - t0; ok = typeof result.avgMs === 'number'; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok, lastCheckTs, ms: ok ? result.avgMs : ms }); return res.json({ ok, ms: ok ? result.avgMs : ms }); } catch (err) { ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms, error: err?.message || String(err) }); } } ms = Date.now() - t0; + const lastCheckTs = Date.now(); + updateUptimeCache(serverId, { ok: false, lastCheckTs, ms }); return res.json({ ok: false, ms }); } catch (error) { const ms = Date.now() - t0; @@ -1462,4 +1536,5 @@ module.exports = { getAddressLists, applyAddressListSummary, uptimeCheck, + getUptimeCache, }; diff --git a/backend/server.js b/backend/server.js index 4156350..360b199 100644 --- a/backend/server.js +++ b/backend/server.js @@ -471,6 +471,7 @@ app.get('/api/mikrotik/address-lists', mikrotikConfigRoutes.getAddressLists); app.post('/api/mikrotik/address-lists/apply-summary', writeLimiter, mikrotikConfigRoutes.applyAddressListSummary); // === UPTIME MONITOR (проверка доступности: http / internal-ping / external-ping из настроек) === +app.get('/api/uptime/cache', mikrotikConfigRoutes.getUptimeCache); app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck); // === TRAFFIC STATS (MikroTik interfaces by jumphost) === diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index d521e12..14c4bf8 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -86,6 +86,7 @@ export default function SettingsPage() { const [trafficInterfacesError, setTrafficInterfacesError] = useState(''); const [uptimeMonitorIntervalSeconds, setUptimeMonitorIntervalSeconds] = useState('120'); const [uptimeMonitorCheckType, setUptimeMonitorCheckType] = useState('http'); + const [uptimeMonitorCacheSeconds, setUptimeMonitorCacheSeconds] = useState('120'); const [serversList, setServersList] = useState([]); const [sidebarSearch, setSidebarSearch] = useState(''); const [activeSection, setActiveSection] = useState(() => { @@ -260,6 +261,9 @@ export default function SettingsPage() { setUptimeMonitorCheckType( checkType === 'internal-ping' || checkType === 'external-ping' ? checkType : 'http' ); + setUptimeMonitorCacheSeconds( + data?.uptimeMonitorCacheSeconds != null ? String(data.uptimeMonitorCacheSeconds) : '120' + ); const raw = data?.trafficInterfaces; setTrafficInterfacesSelected( Array.isArray(raw) @@ -370,6 +374,10 @@ export default function SettingsPage() { uptimeMonitorCheckType === 'internal-ping' || uptimeMonitorCheckType === 'external-ping' ? uptimeMonitorCheckType : 'http', + uptimeMonitorCacheSeconds: Math.max( + 0, + parseInt(uptimeMonitorCacheSeconds, 10) || 120 + ), trafficInterfaces: Array.isArray(trafficInterfacesSelected) ? trafficInterfacesSelected.map((p) => ({ serverKey: p.serverKey, @@ -762,6 +770,20 @@ export default function SettingsPage() { HTTP: подключение к RouterOS API. Внутренний: пинг через туннель (как на карте сети). Внешний: пинг внешнего IP с другого jumphost. +