diff --git a/backend/routes/alertsRoutes.js b/backend/routes/alertsRoutes.js
new file mode 100644
index 0000000..87c3dac
--- /dev/null
+++ b/backend/routes/alertsRoutes.js
@@ -0,0 +1,144 @@
+/**
+ * Система оповещений: агрегирует проблемы из availability и resources/stats.
+ * Критерии проблем: сервер недоступен, MikroTik недоступен, высокое CPU/RAM/HDD.
+ */
+
+const { sendError } = require('../middleware/errorHandler');
+const { readServersFromS3 } = require('./serversRoutes');
+const { checkOneServerFast } = require('./miscRoutes');
+const { getResourceStatsData } = require('./resourceStatsRoutes');
+
+/** Пороги для ресурсов (проценты). */
+const THRESHOLD_CPU = 85;
+const THRESHOLD_RAM = 85;
+const THRESHOLD_HDD = 90;
+
+/**
+ * GET /api/alerts
+ * Возвращает список активных оповещений.
+ */
+async function getAlerts(req, res) {
+ try {
+ const servers = await readServersFromS3();
+ const alerts = [];
+ const now = new Date().toISOString();
+
+ // 1) Доступность серверов (TCP 80/443)
+ const availabilityChecks = await Promise.allSettled(
+ servers.map((s) => checkOneServerFast(s))
+ );
+ servers.forEach((server, i) => {
+ const online = availabilityChecks[i].status === 'fulfilled' && availabilityChecks[i].value;
+ if (!online) {
+ const name = server.name || server.dns || server.ip || server.id || `Сервер #${i + 1}`;
+ alerts.push({
+ id: `server-offline-${server.id || server.dns || server.ip || i}`,
+ type: 'server_offline',
+ severity: 'critical',
+ title: 'Сервер недоступен',
+ description: `${name} не отвечает на TCP (80/443).`,
+ entity: name,
+ entityId: server.id || server.dns || server.ip,
+ link: '/servers',
+ at: now,
+ });
+ }
+ });
+
+ // 2) Ресурсы роутеров (MikroTik): ошибка доступа, высокое CPU/RAM/HDD
+ let resourceData;
+ try {
+ resourceData = await getResourceStatsData();
+ } catch (err) {
+ console.warn('alerts: getResourceStatsData failed', err?.message);
+ resourceData = { routers: [] };
+ }
+
+ const routers = resourceData.routers || [];
+ routers.forEach((router) => {
+ const { serverId, name, error, resource } = router;
+ const entityName = name || serverId;
+
+ if (error) {
+ alerts.push({
+ id: `mikrotik-error-${serverId}`,
+ type: 'mikrotik_unreachable',
+ severity: 'critical',
+ title: 'MikroTik недоступен',
+ description: `${entityName}: ${error}`,
+ entity: entityName,
+ entityId: serverId,
+ link: '/resource-stats',
+ at: now,
+ });
+ return;
+ }
+
+ if (!resource) return;
+
+ if (resource.cpuLoad != null && resource.cpuLoad >= THRESHOLD_CPU) {
+ alerts.push({
+ id: `cpu-high-${serverId}`,
+ type: 'high_cpu',
+ severity: 'warning',
+ title: 'Высокое использование CPU',
+ description: `${entityName}: ${resource.cpuLoad}% (порог ${THRESHOLD_CPU}%).`,
+ entity: entityName,
+ entityId: serverId,
+ value: resource.cpuLoad,
+ threshold: THRESHOLD_CPU,
+ link: '/resource-stats',
+ at: now,
+ });
+ }
+ if (resource.memoryUsagePercent != null && resource.memoryUsagePercent >= THRESHOLD_RAM) {
+ alerts.push({
+ id: `ram-high-${serverId}`,
+ type: 'high_ram',
+ severity: 'warning',
+ title: 'Высокое использование RAM',
+ description: `${entityName}: ${resource.memoryUsagePercent}% (порог ${THRESHOLD_RAM}%).`,
+ entity: entityName,
+ entityId: serverId,
+ value: resource.memoryUsagePercent,
+ threshold: THRESHOLD_RAM,
+ link: '/resource-stats',
+ at: now,
+ });
+ }
+ if (resource.hddUsagePercent != null && resource.hddUsagePercent >= THRESHOLD_HDD) {
+ alerts.push({
+ id: `hdd-high-${serverId}`,
+ type: 'high_hdd',
+ severity: 'warning',
+ title: 'Высокое использование диска',
+ description: `${entityName}: ${resource.hddUsagePercent}% (порог ${THRESHOLD_HDD}%).`,
+ entity: entityName,
+ entityId: serverId,
+ value: resource.hddUsagePercent,
+ threshold: THRESHOLD_HDD,
+ link: '/resource-stats',
+ at: now,
+ });
+ }
+ });
+
+ return res.json({
+ alerts,
+ total: alerts.length,
+ at: now,
+ });
+ } catch (error) {
+ console.error('getAlerts:', error);
+ return sendError(
+ res,
+ 500,
+ error.message || 'Ошибка загрузки оповещений',
+ 'E_ALERTS'
+ );
+ }
+}
+
+module.exports = {
+ getAlerts,
+};
diff --git a/backend/routes/miscRoutes.js b/backend/routes/miscRoutes.js
index 9c46110..781d4ee 100644
--- a/backend/routes/miscRoutes.js
+++ b/backend/routes/miscRoutes.js
@@ -776,5 +776,6 @@ module.exports = {
getPingServicesList,
getPingServices,
refreshPingServicesCache,
+ checkOneServerFast,
};
diff --git a/backend/routes/resourceStatsRoutes.js b/backend/routes/resourceStatsRoutes.js
index 5a7d4ed..7394e8c 100644
--- a/backend/routes/resourceStatsRoutes.js
+++ b/backend/routes/resourceStatsRoutes.js
@@ -74,13 +74,11 @@ function parseResource(raw) {
}
/**
- * GET /api/resources/stats
- * По каждому jumphost/home с MikroTik — запрос system/resource, ответ с группировкой по роутерам.
+ * Внутренняя функция: возвращает данные по ресурсам роутеров (без HTTP).
*/
-async function getResourceStats(req, res) {
- try {
- const servers = await readServersFromS3();
- const routers = (Array.isArray(servers) ? servers : []).filter(
+async function getResourceStatsData() {
+ const servers = await readServersFromS3();
+ const routers = (Array.isArray(servers) ? servers : []).filter(
(s) =>
s &&
(String(s.type || '').toLowerCase() === 'jumphost' ||
@@ -132,7 +130,17 @@ async function getResourceStats(req, res) {
})
);
- return res.json({ routers: results });
+ return { routers: results };
+}
+
+/**
+ * GET /api/resources/stats
+ * По каждому jumphost/home с MikroTik — запрос system/resource, ответ с группировкой по роутерам.
+ */
+async function getResourceStats(req, res) {
+ try {
+ const data = await getResourceStatsData();
+ return res.json(data);
} catch (error) {
console.error('getResourceStats:', error);
return sendError(
@@ -146,4 +154,5 @@ async function getResourceStats(req, res) {
module.exports = {
getResourceStats,
+ getResourceStatsData,
};
diff --git a/backend/server.js b/backend/server.js
index 627f8fe..3431fa3 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -31,6 +31,7 @@ const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
const trafficRoutes = require('./routes/trafficRoutes');
const resourceStatsRoutes = require('./routes/resourceStatsRoutes');
+const alertsRoutes = require('./routes/alertsRoutes');
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
@@ -483,6 +484,9 @@ app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats);
// === RESOURCE STATS (RAM/CPU/HDD по роутерам) ===
app.get('/api/resources/stats', resourceStatsRoutes.getResourceStats);
+// === ALERTS (агрегация проблем: серверы, ресурсы роутеров) ===
+app.get('/api/alerts', alertsRoutes.getAlerts);
+
// === MIKROTIK BACKUPS (S3) ===
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 3ac8aa3..f7d64d0 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -58,6 +58,8 @@ import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPal
import ErrorBoundary from './components/ErrorBoundary.jsx';
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
import { PingProvider } from './contexts/PingContext.jsx';
+import { AlertsProvider } from './contexts/AlertsContext.jsx';
+import AlertsBell from './components/AlertsBell.jsx';
// --- Simple i18n (RU/EN) ---
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
@@ -124,12 +126,14 @@ function App() {