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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user