feat(ResourceStats): add resource statistics API endpoint and integrate into frontend with routing and localization updates
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m35s

This commit is contained in:
2026-02-22 22:05:47 +07:00
parent 484dd56312
commit 9798e7fe95
4 changed files with 396 additions and 2 deletions
+149
View File
@@ -0,0 +1,149 @@
/**
* Роуты для сбора статистики ресурсов (RAM, CPU, HDD) с MikroTik RouterOS.
* GET /api/resources/stats — system/resource по каждому jumphost/home с учётами MikroTik.
*/
const { sendError } = require('../middleware/errorHandler');
const { decrypt } = require('../utils/encryption');
const { readServersFromS3 } = require('./serversRoutes');
const { createRosClient } = require('../services/mikrotikApplyService');
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 };
}
/**
* Парсинг system/resource в нормализованный объект для UI.
* MikroTik REST возвращает: total-memory, free-memory (bytes), cpu-load (%),
* total-hdd-space, free-hdd-space (bytes), uptime, version, board-name и др.
*/
function parseResource(raw) {
if (!raw || typeof raw !== 'object') return null;
const num = (v) => (v != null && v !== '' ? parseInt(String(v), 10) : null);
const totalMemory = num(raw['total-memory']);
const freeMemory = num(raw['free-memory']);
const usedMemory = totalMemory != null && freeMemory != null ? totalMemory - freeMemory : null;
const totalHdd = num(raw['total-hdd-space']);
const freeHdd = num(raw['free-hdd-space']);
const usedHdd = totalHdd != null && freeHdd != null ? totalHdd - freeHdd : null;
const cpuLoad = raw['cpu-load'] != null ? parseFloat(String(raw['cpu-load'])) : null;
return {
// RAM (bytes и проценты)
totalMemory: totalMemory,
freeMemory: freeMemory,
usedMemory: usedMemory,
memoryUsagePercent:
totalMemory != null && totalMemory > 0 && usedMemory != null
? Math.round((usedMemory / totalMemory) * 100)
: null,
// CPU (%)
cpuLoad: cpuLoad != null ? Math.round(cpuLoad) : null,
cpuCount: num(raw['cpu-count']) ?? null,
cpuFrequency: raw['cpu-frequency'] != null ? String(raw['cpu-frequency']) : null,
cpu: raw.cpu != null ? String(raw.cpu) : null,
// HDD (bytes и проценты)
totalHddSpace: totalHdd,
freeHddSpace: freeHdd,
usedHddSpace: usedHdd,
hddUsagePercent:
totalHdd != null && totalHdd > 0 && usedHdd != null
? Math.round((usedHdd / totalHdd) * 100)
: null,
// прочее
uptime: raw.uptime != null ? String(raw.uptime) : null,
version: raw.version != null ? String(raw.version) : null,
boardName: raw['board-name'] != null ? String(raw['board-name']) : null,
architectureName: raw['architecture-name'] != null ? String(raw['architecture-name']) : null,
platform: raw.platform != null ? String(raw.platform) : null,
};
}
/**
* GET /api/resources/stats
* По каждому jumphost/home с MikroTik — запрос system/resource, ответ с группировкой по роутерам.
*/
async function getResourceStats(req, res) {
try {
const servers = await readServersFromS3();
const routers = (Array.isArray(servers) ? servers : []).filter(
(s) =>
s &&
(String(s.type || '').toLowerCase() === 'jumphost' ||
String(s.type || '').toLowerCase() === 'home')
);
const results = await Promise.all(
routers.map(async (server) => {
const serverId = server.id || server.dns || server.ip;
const name = server.name || server.dns || server.ip || serverId;
const host = server.mikrotikHost || server.ip || server.dns;
const creds = getMikrotikCredentials(server);
if (!creds) {
return {
serverId,
name,
host,
groupKey: server.groupKey || null,
error: 'MikroTik API не настроен или нет пароля',
resource: null,
};
}
try {
const client = createRosClient(creds);
const apiRes = await client.print('system/resource');
const raw = apiRes?.data;
const single = Array.isArray(raw) ? raw[0] : raw;
const resource = parseResource(single);
return {
serverId,
name,
host,
groupKey: server.groupKey || null,
error: null,
resource,
};
} catch (err) {
const msg = err.response?.data?.message || err.message || 'Ошибка запроса к RouterOS';
return {
serverId,
name,
host,
groupKey: server.groupKey || null,
error: msg,
resource: null,
};
}
})
);
return res.json({ routers: results });
} catch (error) {
console.error('getResourceStats:', error);
return sendError(
res,
500,
error.message || 'Ошибка сбора статистики ресурсов',
'E_RESOURCE_STATS'
);
}
}
module.exports = {
getResourceStats,
};
+4
View File
@@ -30,6 +30,7 @@ const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
const trafficRoutes = require('./routes/trafficRoutes');
const resourceStatsRoutes = require('./routes/resourceStatsRoutes');
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
@@ -479,6 +480,9 @@ app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck);
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats);
// === RESOURCE STATS (RAM/CPU/HDD по роутерам) ===
app.get('/api/resources/stats', resourceStatsRoutes.getResourceStats);
// === MIKROTIK BACKUPS (S3) ===
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);