From 5c03ec7bd0d19817aca951d6d7a66f48498417fa Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 17 Feb 2026 23:18:21 +0700 Subject: [PATCH] feat(TrafficRoutes, InterfaceSpeedTest, Settings): implement MikroTik interface statistics route; update speed test duration settings to default to 5 seconds across UI components --- backend/routes/trafficRoutes.js | 118 ++++++++++++++++++++++ frontend/src/InterfaceSpeedTest.jsx | 2 +- frontend/src/SettingsPage.jsx | 6 +- frontend/src/components/SettingsModal.jsx | 6 +- 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/backend/routes/trafficRoutes.js b/backend/routes/trafficRoutes.js index cb8a959..0ee8f6e 100644 --- a/backend/routes/trafficRoutes.js +++ b/backend/routes/trafficRoutes.js @@ -1,3 +1,121 @@ +/** + * Роуты для сбора статистики трафика с MikroTik RouterOS (интерфейсы по jumphost). + * GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost. + */ + +const { sendError } = require('../middleware/errorHandler'); +const { decrypt } = require('../utils/encryption'); +const { readServersFromS3 } = require('./serversRoutes'); +const { createRosClient } = require('../services/mikrotikApplyService'); + +/** Получить MikroTik credentials из сервера (jumphost или home) */ +function getMikrotikCredentials(server) { + if (!server || (server.type !== 'jumphost' && server.type !== 'home')) return null; + const host = server.mikrotikHost || server.ip || server.dns; + if (!host) return null; + const port = server.mikrotikPort; + const restPort = port === 80 || port === 443 || port === 8443 ? port : 80; + const user = server.mikrotikUser || 'admin'; + let password = ''; + if (server.encryptedMikrotikPassword) { + try { + password = decrypt(server.encryptedMikrotikPassword); + } catch (_) { + return null; + } + } + return { host, port: restPort, user, password, secure: false }; +} + +/** + * GET /api/traffic/interface-stats + * Возвращает по каждому jumphost список интерфейсов с rx-byte, tx-byte (из RouterOS REST /interface). + */ +async function getInterfaceStats(req, res) { + try { + const servers = await readServersFromS3(); + const jumphosts = (Array.isArray(servers) ? servers : []).filter( + (s) => + s && + (String(s.type || '').toLowerCase() === 'jumphost' || + String(s.type || '').toLowerCase() === 'home') + ); + + const results = await Promise.all( + jumphosts.map(async (server) => { + const creds = getMikrotikCredentials(server); + if (!creds) { + return { + serverId: server.id, + name: server.name || server.dns || server.ip || server.id, + host: server.mikrotikHost || server.ip || server.dns, + error: 'MikroTik API не настроен или нет пароля', + interfaces: [], + }; + } + try { + const client = createRosClient(creds); + const apiRes = await client.print('interface'); + const raw = apiRes?.data; + const list = Array.isArray(raw) ? raw : raw ? [raw] : []; + const interfaces = list + .map((iface) => { + const rx = + iface['rx-byte'] != null + ? parseInt(String(iface['rx-byte']), 10) + : 0; + const tx = + iface['tx-byte'] != null + ? parseInt(String(iface['tx-byte']), 10) + : 0; + const total = rx + tx; + return { + name: iface.name != null ? String(iface.name) : '', + type: iface.type != null ? String(iface.type) : '', + rxBytes: Number.isNaN(rx) ? 0 : rx, + txBytes: Number.isNaN(tx) ? 0 : tx, + totalBytes: Number.isNaN(total) ? 0 : total, + }; + }) + .filter((i) => i.name && i.totalBytes >= 0); + return { + serverId: server.id, + name: server.name || server.dns || server.ip || server.id, + host: server.mikrotikHost || server.ip || server.dns, + interfaces, + }; + } catch (err) { + const msg = + err.response?.data?.message || + err.message || + 'Ошибка запроса к RouterOS'; + return { + serverId: server.id, + name: server.name || server.dns || server.ip || server.id, + host: server.mikrotikHost || server.ip || server.dns, + error: msg, + interfaces: [], + }; + } + }) + ); + + return res.json({ jumphosts: results }); + } catch (error) { + console.error('getInterfaceStats:', error); + return sendError( + res, + 500, + error.message || 'Ошибка сбора статистики трафика', + 'E_TRAFFIC' + ); + } +} + +module.exports = { + getInterfaceStats, +}; + /** * Роуты для сбора статистики трафика с MikroTik RouterOS (интерфейсы по jumphost). * GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost. diff --git a/frontend/src/InterfaceSpeedTest.jsx b/frontend/src/InterfaceSpeedTest.jsx index 398de50..e654a4f 100644 --- a/frontend/src/InterfaceSpeedTest.jsx +++ b/frontend/src/InterfaceSpeedTest.jsx @@ -56,7 +56,7 @@ export default function InterfaceSpeedTest() { 1, Math.min( 600, - parseInt(ui.interfaceSpeedTestDurationSeconds, 10) || 10 + parseInt(ui.interfaceSpeedTestDurationSeconds, 10) || 5 ) ); const cacheMinutes = Math.max( diff --git a/frontend/src/SettingsPage.jsx b/frontend/src/SettingsPage.jsx index 3100d16..fc68a23 100644 --- a/frontend/src/SettingsPage.jsx +++ b/frontend/src/SettingsPage.jsx @@ -240,7 +240,7 @@ export default function SettingsPage() { setInterfaceSpeedTestDurationSeconds( data?.interfaceSpeedTestDurationSeconds != null ? String(data.interfaceSpeedTestDurationSeconds) - : '10' + : '5' ); setInterfaceSpeedTestCacheMinutes( data?.interfaceSpeedTestCacheMinutes != null @@ -343,7 +343,7 @@ export default function SettingsPage() { interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp', interfaceSpeedTestDurationSeconds: Math.max( 1, - parseInt(interfaceSpeedTestDurationSeconds, 10) || 10 + parseInt(interfaceSpeedTestDurationSeconds, 10) || 5 ), interfaceSpeedTestCacheMinutes: Math.max( 0, @@ -608,7 +608,7 @@ export default function SettingsPage() { type="number" value={interfaceSpeedTestDurationSeconds} onChange={setInterfaceSpeedTestDurationSeconds} - placeholder="10" + placeholder="5" helpText="Интервал, за который измеряется средняя скорость по интерфейсу." disabled={saving} min={1} diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index 5cbd258..700bfd5 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -78,7 +78,7 @@ export default function SettingsModal({ open, onClose }) { setInterfaceSpeedTestDurationSeconds( data?.interfaceSpeedTestDurationSeconds != null ? String(data.interfaceSpeedTestDurationSeconds) - : '10' + : '5' ); setInterfaceSpeedTestCacheMinutes( data?.interfaceSpeedTestCacheMinutes != null @@ -161,7 +161,7 @@ export default function SettingsModal({ open, onClose }) { interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp', interfaceSpeedTestDurationSeconds: Math.max( 1, - parseInt(interfaceSpeedTestDurationSeconds, 10) || 10 + parseInt(interfaceSpeedTestDurationSeconds, 10) || 5 ), interfaceSpeedTestCacheMinutes: Math.max( 0, @@ -317,7 +317,7 @@ export default function SettingsModal({ open, onClose }) { type="number" value={interfaceSpeedTestDurationSeconds} onChange={setInterfaceSpeedTestDurationSeconds} - placeholder="10" + placeholder="5" helpText="Интервал, за который измеряется средняя скорость по интерфейсу." disabled={loading || saving} min={1}