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).
|
* Роуты для сбора статистики трафика с MikroTik RouterOS (интерфейсы по jumphost).
|
||||||
* GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost.
|
* GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function InterfaceSpeedTest() {
|
|||||||
1,
|
1,
|
||||||
Math.min(
|
Math.min(
|
||||||
600,
|
600,
|
||||||
parseInt(ui.interfaceSpeedTestDurationSeconds, 10) || 10
|
parseInt(ui.interfaceSpeedTestDurationSeconds, 10) || 5
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const cacheMinutes = Math.max(
|
const cacheMinutes = Math.max(
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export default function SettingsPage() {
|
|||||||
setInterfaceSpeedTestDurationSeconds(
|
setInterfaceSpeedTestDurationSeconds(
|
||||||
data?.interfaceSpeedTestDurationSeconds != null
|
data?.interfaceSpeedTestDurationSeconds != null
|
||||||
? String(data.interfaceSpeedTestDurationSeconds)
|
? String(data.interfaceSpeedTestDurationSeconds)
|
||||||
: '10'
|
: '5'
|
||||||
);
|
);
|
||||||
setInterfaceSpeedTestCacheMinutes(
|
setInterfaceSpeedTestCacheMinutes(
|
||||||
data?.interfaceSpeedTestCacheMinutes != null
|
data?.interfaceSpeedTestCacheMinutes != null
|
||||||
@@ -343,7 +343,7 @@ export default function SettingsPage() {
|
|||||||
interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp',
|
interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp',
|
||||||
interfaceSpeedTestDurationSeconds: Math.max(
|
interfaceSpeedTestDurationSeconds: Math.max(
|
||||||
1,
|
1,
|
||||||
parseInt(interfaceSpeedTestDurationSeconds, 10) || 10
|
parseInt(interfaceSpeedTestDurationSeconds, 10) || 5
|
||||||
),
|
),
|
||||||
interfaceSpeedTestCacheMinutes: Math.max(
|
interfaceSpeedTestCacheMinutes: Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -608,7 +608,7 @@ export default function SettingsPage() {
|
|||||||
type="number"
|
type="number"
|
||||||
value={interfaceSpeedTestDurationSeconds}
|
value={interfaceSpeedTestDurationSeconds}
|
||||||
onChange={setInterfaceSpeedTestDurationSeconds}
|
onChange={setInterfaceSpeedTestDurationSeconds}
|
||||||
placeholder="10"
|
placeholder="5"
|
||||||
helpText="Интервал, за который измеряется средняя скорость по интерфейсу."
|
helpText="Интервал, за который измеряется средняя скорость по интерфейсу."
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
setInterfaceSpeedTestDurationSeconds(
|
setInterfaceSpeedTestDurationSeconds(
|
||||||
data?.interfaceSpeedTestDurationSeconds != null
|
data?.interfaceSpeedTestDurationSeconds != null
|
||||||
? String(data.interfaceSpeedTestDurationSeconds)
|
? String(data.interfaceSpeedTestDurationSeconds)
|
||||||
: '10'
|
: '5'
|
||||||
);
|
);
|
||||||
setInterfaceSpeedTestCacheMinutes(
|
setInterfaceSpeedTestCacheMinutes(
|
||||||
data?.interfaceSpeedTestCacheMinutes != null
|
data?.interfaceSpeedTestCacheMinutes != null
|
||||||
@@ -161,7 +161,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp',
|
interfaceSpeedTestProtocol === 'udp' ? 'udp' : 'tcp',
|
||||||
interfaceSpeedTestDurationSeconds: Math.max(
|
interfaceSpeedTestDurationSeconds: Math.max(
|
||||||
1,
|
1,
|
||||||
parseInt(interfaceSpeedTestDurationSeconds, 10) || 10
|
parseInt(interfaceSpeedTestDurationSeconds, 10) || 5
|
||||||
),
|
),
|
||||||
interfaceSpeedTestCacheMinutes: Math.max(
|
interfaceSpeedTestCacheMinutes: Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -317,7 +317,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
type="number"
|
type="number"
|
||||||
value={interfaceSpeedTestDurationSeconds}
|
value={interfaceSpeedTestDurationSeconds}
|
||||||
onChange={setInterfaceSpeedTestDurationSeconds}
|
onChange={setInterfaceSpeedTestDurationSeconds}
|
||||||
placeholder="10"
|
placeholder="5"
|
||||||
helpText="Интервал, за который измеряется средняя скорость по интерфейсу."
|
helpText="Интервал, за который измеряется средняя скорость по интерфейсу."
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
Reference in New Issue
Block a user