feat(TrafficRoutes, InterfaceSpeedTest, Settings): implement MikroTik interface statistics route; update speed test duration settings to default to 5 seconds across UI components
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-02-17 23:18:21 +07:00
parent 31ed777334
commit 5c03ec7bd0
4 changed files with 125 additions and 7 deletions
+118
View File
@@ -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.
+1 -1
View File
@@ -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(
+3 -3
View File
@@ -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}
+3 -3
View File
@@ -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}