fix: исправление тултипов Chart.js и обновление проекта
- Исправлено скрытие тултипов при уходе курсора влево/вправо - Добавлена проверка всех 4 границ chartArea (caretX + caretY) - Добавлен глобальный mousemove обработчик (crosshair overlay перехватывал mouseleave) - Добавлен плагин chartjs-plugin-crosshair.min.js - Обновлён дамп БД: только структура + примеры данных (без реальных данных) - Добавлены: FlashMiddleware, NotificationService, .gitignore - Обновлены: контроллеры, модели, middlewares, шаблоны - Удалены: detail.twig.bak файлы Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
9b64cee32c
commit
b875e57e4c
|
|
@ -0,0 +1,3 @@
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "src/",
|
"App\\": "src/",
|
||||||
|
"App\\Services\\": "src/Services",
|
||||||
"Config\\": "config/"
|
"Config\\": "config/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,6 @@ CREATE TABLE `agent_configs` (
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `agent_configs`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `agent_configs` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `agent_configs` DISABLE KEYS */;
|
|
||||||
INSERT INTO `agent_configs` VALUES
|
|
||||||
(1,1,60,'[]',1,'2026-02-14 05:49:36','2026-02-14 05:49:36'),
|
|
||||||
(2,2,60,'[\"containerd.service\",\"nginx.service\",\"php8.3-fpm.service\"]',1,'2026-02-14 13:44:26','2026-02-14 13:52:50');
|
|
||||||
/*!40000 ALTER TABLE `agent_configs` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `agent_tokens`
|
-- Table structure for table `agent_tokens`
|
||||||
--
|
--
|
||||||
|
|
@ -69,18 +57,6 @@ CREATE TABLE `agent_tokens` (
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `agent_tokens`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `agent_tokens` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `agent_tokens` DISABLE KEYS */;
|
|
||||||
INSERT INTO `agent_tokens` VALUES
|
|
||||||
(2,1,'37bbd66034cf35cdad603126c85c7c432cbb8ccac0873c237524a1aebfda7ccc','tPj3ysWuvze/grJ91skKP/iUD46n6mqMm/iXqjrQ3dKbBbm0/wmGrVKe8FggurfXBZ5zue3d4hE8t9qyYjBMNw==','2026-02-03 07:55:08',NULL),
|
|
||||||
(3,2,'09e4fcabdc8aa915a75ffaecfb3a7589755847f59326abf1fdfaff14e284ee5c',NULL,'2026-02-14 09:55:18','2026-02-14 17:23:13');
|
|
||||||
/*!40000 ALTER TABLE `agent_tokens` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `alerts`
|
-- Table structure for table `alerts`
|
||||||
--
|
--
|
||||||
|
|
@ -104,13 +80,31 @@ CREATE TABLE `alerts` (
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping data for table `alerts`
|
-- Table structure for table `global_notification_settings`
|
||||||
--
|
--
|
||||||
|
|
||||||
LOCK TABLES `alerts` WRITE;
|
DROP TABLE IF EXISTS `global_notification_settings`;
|
||||||
/*!40000 ALTER TABLE `alerts` DISABLE KEYS */;
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
/*!40000 ALTER TABLE `alerts` ENABLE KEYS */;
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
UNLOCK TABLES;
|
CREATE TABLE `global_notification_settings` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`smtp_host` varchar(255) DEFAULT '',
|
||||||
|
`smtp_port` int(11) DEFAULT 587,
|
||||||
|
`smtp_username` varchar(255) DEFAULT '',
|
||||||
|
`smtp_password` varchar(255) DEFAULT '',
|
||||||
|
`smtp_encryption` enum('tls','ssl','none') DEFAULT 'tls',
|
||||||
|
`smtp_from_email` varchar(255) DEFAULT '',
|
||||||
|
`telegram_bot_token` varchar(255) DEFAULT '',
|
||||||
|
`telegram_chat_id` varchar(100) DEFAULT '',
|
||||||
|
`telegram_proxy` varchar(255) DEFAULT 'http://127.0.0.1:1081',
|
||||||
|
`email_enabled` tinyint(1) DEFAULT 0,
|
||||||
|
`telegram_enabled` tinyint(1) DEFAULT 0,
|
||||||
|
`notify_on_warning` tinyint(1) DEFAULT 1,
|
||||||
|
`notify_on_critical` tinyint(1) DEFAULT 1,
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `metric_names`
|
-- Table structure for table `metric_names`
|
||||||
|
|
@ -126,24 +120,9 @@ CREATE TABLE `metric_names` (
|
||||||
`description` text DEFAULT NULL,
|
`description` text DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `name` (`name`)
|
UNIQUE KEY `name` (`name`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `metric_names`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `metric_names` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `metric_names` DISABLE KEYS */;
|
|
||||||
INSERT INTO `metric_names` VALUES
|
|
||||||
(1,'cpu_load','%','Загрузка процессора'),
|
|
||||||
(2,'ram_used','%','Использование оперативной памяти'),
|
|
||||||
(3,'disk_used','%','Использование диска'),
|
|
||||||
(4,'network_in','MB/s','Скорость приема сети'),
|
|
||||||
(5,'network_out','MB/s','Скорость передачи сети');
|
|
||||||
/*!40000 ALTER TABLE `metric_names` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `metric_thresholds`
|
-- Table structure for table `metric_thresholds`
|
||||||
--
|
--
|
||||||
|
|
@ -166,15 +145,6 @@ CREATE TABLE `metric_thresholds` (
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `metric_thresholds`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `metric_thresholds` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `metric_thresholds` DISABLE KEYS */;
|
|
||||||
/*!40000 ALTER TABLE `metric_thresholds` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `server_groups`
|
-- Table structure for table `server_groups`
|
||||||
--
|
--
|
||||||
|
|
@ -192,18 +162,6 @@ CREATE TABLE `server_groups` (
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `server_groups`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `server_groups` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `server_groups` DISABLE KEYS */;
|
|
||||||
INSERT INTO `server_groups` VALUES
|
|
||||||
(1,'Default','Общая группа','fa-servers','#5fad47'),
|
|
||||||
(2,'1','','','#5b2067');
|
|
||||||
/*!40000 ALTER TABLE `server_groups` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `server_metrics`
|
-- Table structure for table `server_metrics`
|
||||||
--
|
--
|
||||||
|
|
@ -222,285 +180,9 @@ CREATE TABLE `server_metrics` (
|
||||||
KEY `metric_name_id` (`metric_name_id`),
|
KEY `metric_name_id` (`metric_name_id`),
|
||||||
CONSTRAINT `server_metrics_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE,
|
CONSTRAINT `server_metrics_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE,
|
||||||
CONSTRAINT `server_metrics_ibfk_2` FOREIGN KEY (`metric_name_id`) REFERENCES `metric_names` (`id`)
|
CONSTRAINT `server_metrics_ibfk_2` FOREIGN KEY (`metric_name_id`) REFERENCES `metric_names` (`id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=326 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=395224 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `server_metrics`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `server_metrics` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `server_metrics` DISABLE KEYS */;
|
|
||||||
INSERT INTO `server_metrics` VALUES
|
|
||||||
(1,2,1,'15.50','2026-02-14 10:05:07'),
|
|
||||||
(2,2,2,'40.20','2026-02-14 10:05:07'),
|
|
||||||
(3,2,3,'25.30','2026-02-14 10:05:07'),
|
|
||||||
(4,2,1,'1.80','2026-02-14 10:05:13'),
|
|
||||||
(5,2,2,'39.50','2026-02-14 10:05:13'),
|
|
||||||
(6,2,3,'17.80','2026-02-14 10:05:13'),
|
|
||||||
(7,2,1,'1.50','2026-02-14 10:06:14'),
|
|
||||||
(8,2,2,'39.60','2026-02-14 10:06:14'),
|
|
||||||
(9,2,3,'17.80','2026-02-14 10:06:14'),
|
|
||||||
(10,2,1,'0.50','2026-02-14 10:06:19'),
|
|
||||||
(11,2,2,'39.70','2026-02-14 10:06:19'),
|
|
||||||
(12,2,3,'17.80','2026-02-14 10:06:19'),
|
|
||||||
(13,2,1,'45.50','2026-02-14 10:07:10'),
|
|
||||||
(14,2,2,'55.20','2026-02-14 10:07:10'),
|
|
||||||
(15,2,3,'65.10','2026-02-14 10:07:10'),
|
|
||||||
(16,2,1,'0.80','2026-02-14 10:07:15'),
|
|
||||||
(17,2,2,'39.50','2026-02-14 10:07:15'),
|
|
||||||
(18,2,3,'17.80','2026-02-14 10:07:15'),
|
|
||||||
(19,2,1,'45.50','2026-02-14 10:08:14'),
|
|
||||||
(20,2,2,'55.20','2026-02-14 10:08:14'),
|
|
||||||
(21,2,1,'0.70','2026-02-14 10:08:17'),
|
|
||||||
(22,2,2,'39.70','2026-02-14 10:08:17'),
|
|
||||||
(23,2,3,'17.80','2026-02-14 10:08:17'),
|
|
||||||
(24,2,1,'0.30','2026-02-14 10:09:18'),
|
|
||||||
(25,2,2,'39.80','2026-02-14 10:09:18'),
|
|
||||||
(26,2,3,'17.80','2026-02-14 10:09:18'),
|
|
||||||
(27,2,1,'0.50','2026-02-14 10:10:19'),
|
|
||||||
(28,2,2,'40.80','2026-02-14 10:10:19'),
|
|
||||||
(29,2,3,'17.80','2026-02-14 10:10:19'),
|
|
||||||
(30,2,1,'0.20','2026-02-14 10:11:21'),
|
|
||||||
(31,2,2,'39.00','2026-02-14 10:11:21'),
|
|
||||||
(32,2,3,'17.80','2026-02-14 10:11:21'),
|
|
||||||
(33,2,1,'0.50','2026-02-14 10:12:22'),
|
|
||||||
(34,2,2,'39.00','2026-02-14 10:12:22'),
|
|
||||||
(35,2,3,'17.80','2026-02-14 10:12:22'),
|
|
||||||
(36,2,1,'5.80','2026-02-14 10:13:24'),
|
|
||||||
(37,2,2,'39.20','2026-02-14 10:13:24'),
|
|
||||||
(38,2,3,'17.80','2026-02-14 10:13:24'),
|
|
||||||
(39,2,1,'1.00','2026-02-14 10:14:25'),
|
|
||||||
(40,2,2,'39.50','2026-02-14 10:14:25'),
|
|
||||||
(41,2,3,'17.80','2026-02-14 10:14:25'),
|
|
||||||
(42,2,1,'0.50','2026-02-14 10:15:26'),
|
|
||||||
(43,2,2,'39.60','2026-02-14 10:15:26'),
|
|
||||||
(44,2,3,'17.80','2026-02-14 10:15:26'),
|
|
||||||
(45,2,1,'0.30','2026-02-14 10:16:28'),
|
|
||||||
(46,2,2,'39.60','2026-02-14 10:16:28'),
|
|
||||||
(47,2,3,'17.80','2026-02-14 10:16:28'),
|
|
||||||
(48,2,1,'0.50','2026-02-14 10:17:29'),
|
|
||||||
(49,2,2,'39.60','2026-02-14 10:17:29'),
|
|
||||||
(50,2,3,'17.80','2026-02-14 10:17:29'),
|
|
||||||
(51,2,1,'0.50','2026-02-14 10:18:30'),
|
|
||||||
(52,2,2,'39.80','2026-02-14 10:18:30'),
|
|
||||||
(53,2,3,'17.80','2026-02-14 10:18:30'),
|
|
||||||
(54,2,1,'0.30','2026-02-14 10:19:32'),
|
|
||||||
(55,2,2,'39.50','2026-02-14 10:19:32'),
|
|
||||||
(56,2,3,'17.80','2026-02-14 10:19:32'),
|
|
||||||
(57,2,1,'0.30','2026-02-14 10:20:33'),
|
|
||||||
(58,2,2,'39.70','2026-02-14 10:20:33'),
|
|
||||||
(59,2,3,'17.80','2026-02-14 10:20:33'),
|
|
||||||
(60,2,1,'1.00','2026-02-14 10:21:35'),
|
|
||||||
(61,2,2,'39.90','2026-02-14 10:21:35'),
|
|
||||||
(62,2,3,'17.80','2026-02-14 10:21:35'),
|
|
||||||
(63,2,1,'1.00','2026-02-14 10:22:36'),
|
|
||||||
(64,2,2,'39.90','2026-02-14 10:22:36'),
|
|
||||||
(65,2,3,'17.80','2026-02-14 10:22:36'),
|
|
||||||
(66,2,1,'0.80','2026-02-14 10:23:37'),
|
|
||||||
(67,2,2,'39.90','2026-02-14 10:23:37'),
|
|
||||||
(68,2,3,'17.80','2026-02-14 10:23:37'),
|
|
||||||
(69,2,1,'0.80','2026-02-14 10:24:39'),
|
|
||||||
(70,2,2,'39.90','2026-02-14 10:24:39'),
|
|
||||||
(71,2,3,'17.80','2026-02-14 10:24:39'),
|
|
||||||
(72,2,1,'0.00','2026-02-14 10:25:40'),
|
|
||||||
(73,2,2,'40.20','2026-02-14 10:25:40'),
|
|
||||||
(74,2,3,'17.80','2026-02-14 10:25:40'),
|
|
||||||
(75,2,1,'0.50','2026-02-14 16:49:57'),
|
|
||||||
(76,2,2,'40.40','2026-02-14 16:49:57'),
|
|
||||||
(77,2,3,'17.80','2026-02-14 16:49:57'),
|
|
||||||
(78,2,1,'0.00','2026-02-14 16:50:58'),
|
|
||||||
(79,2,2,'40.60','2026-02-14 16:50:58'),
|
|
||||||
(80,2,3,'17.80','2026-02-14 16:50:58'),
|
|
||||||
(81,2,1,'1.70','2026-02-14 16:52:00'),
|
|
||||||
(82,2,2,'40.40','2026-02-14 16:52:00'),
|
|
||||||
(83,2,3,'17.80','2026-02-14 16:52:00'),
|
|
||||||
(84,2,1,'0.70','2026-02-14 16:53:01'),
|
|
||||||
(85,2,2,'40.40','2026-02-14 16:53:01'),
|
|
||||||
(86,2,3,'17.80','2026-02-14 16:53:01'),
|
|
||||||
(87,2,1,'0.50','2026-02-14 16:54:03'),
|
|
||||||
(88,2,2,'40.40','2026-02-14 16:54:03'),
|
|
||||||
(89,2,3,'17.80','2026-02-14 16:54:03'),
|
|
||||||
(90,2,1,'0.00','2026-02-14 16:55:04'),
|
|
||||||
(91,2,2,'39.30','2026-02-14 16:55:04'),
|
|
||||||
(92,2,3,'17.80','2026-02-14 16:55:04'),
|
|
||||||
(93,2,1,'6.50','2026-02-14 16:56:06'),
|
|
||||||
(94,2,2,'41.20','2026-02-14 16:56:06'),
|
|
||||||
(95,2,3,'17.80','2026-02-14 16:56:06'),
|
|
||||||
(96,2,1,'0.50','2026-02-14 16:57:07'),
|
|
||||||
(97,2,2,'39.10','2026-02-14 16:57:07'),
|
|
||||||
(98,2,3,'17.80','2026-02-14 16:57:07'),
|
|
||||||
(99,2,1,'0.80','2026-02-14 16:58:08'),
|
|
||||||
(100,2,2,'39.50','2026-02-14 16:58:08'),
|
|
||||||
(101,2,3,'17.80','2026-02-14 16:58:08'),
|
|
||||||
(102,2,1,'2.70','2026-02-14 16:59:10'),
|
|
||||||
(103,2,2,'40.70','2026-02-14 16:59:10'),
|
|
||||||
(104,2,3,'17.80','2026-02-14 16:59:10'),
|
|
||||||
(105,2,1,'0.80','2026-02-14 17:00:11'),
|
|
||||||
(106,2,2,'41.40','2026-02-14 17:00:11'),
|
|
||||||
(107,2,3,'17.80','2026-02-14 17:00:11'),
|
|
||||||
(108,2,1,'0.50','2026-02-14 17:01:13'),
|
|
||||||
(109,2,2,'40.00','2026-02-14 17:01:13'),
|
|
||||||
(110,2,3,'17.80','2026-02-14 17:01:13'),
|
|
||||||
(111,2,1,'35.70','2026-02-14 17:02:14'),
|
|
||||||
(112,2,2,'39.60','2026-02-14 17:02:14'),
|
|
||||||
(113,2,3,'17.80','2026-02-14 17:02:14'),
|
|
||||||
(114,2,1,'2.00','2026-02-14 17:03:15'),
|
|
||||||
(115,2,2,'40.80','2026-02-14 17:03:15'),
|
|
||||||
(116,2,3,'17.80','2026-02-14 17:03:15'),
|
|
||||||
(117,2,1,'0.50','2026-02-14 17:04:17'),
|
|
||||||
(118,2,2,'40.00','2026-02-14 17:04:17'),
|
|
||||||
(119,2,3,'17.80','2026-02-14 17:04:17'),
|
|
||||||
(120,2,1,'0.80','2026-02-14 17:05:18'),
|
|
||||||
(121,2,2,'40.20','2026-02-14 17:05:18'),
|
|
||||||
(122,2,3,'17.80','2026-02-14 17:05:18'),
|
|
||||||
(123,2,1,'0.70','2026-02-14 17:06:20'),
|
|
||||||
(124,2,2,'40.20','2026-02-14 17:06:20'),
|
|
||||||
(125,2,3,'17.80','2026-02-14 17:06:20'),
|
|
||||||
(126,2,1,'1.00','2026-02-14 17:06:46'),
|
|
||||||
(127,2,2,'40.10','2026-02-14 17:06:46'),
|
|
||||||
(128,2,3,'17.80','2026-02-14 17:06:46'),
|
|
||||||
(129,2,1,'1.20','2026-02-14 17:07:47'),
|
|
||||||
(130,2,2,'40.20','2026-02-14 17:07:47'),
|
|
||||||
(131,2,3,'17.80','2026-02-14 17:07:47'),
|
|
||||||
(132,2,1,'5.30','2026-02-14 17:08:49'),
|
|
||||||
(133,2,2,'40.40','2026-02-14 17:08:49'),
|
|
||||||
(134,2,3,'17.80','2026-02-14 17:08:49'),
|
|
||||||
(135,2,1,'0.80','2026-02-14 17:09:50'),
|
|
||||||
(136,2,2,'40.60','2026-02-14 17:09:50'),
|
|
||||||
(137,2,3,'17.80','2026-02-14 17:09:50'),
|
|
||||||
(138,2,1,'0.50','2026-02-14 17:10:51'),
|
|
||||||
(139,2,2,'40.60','2026-02-14 17:10:51'),
|
|
||||||
(140,2,3,'17.80','2026-02-14 17:10:51'),
|
|
||||||
(141,2,1,'0.50','2026-02-14 17:11:53'),
|
|
||||||
(142,2,2,'40.20','2026-02-14 17:11:53'),
|
|
||||||
(143,2,3,'17.80','2026-02-14 17:11:53'),
|
|
||||||
(144,2,1,'1.50','2026-02-14 17:12:54'),
|
|
||||||
(145,2,2,'39.40','2026-02-14 17:12:54'),
|
|
||||||
(146,2,3,'17.80','2026-02-14 17:12:54'),
|
|
||||||
(147,2,1,'0.50','2026-02-14 17:13:55'),
|
|
||||||
(148,2,2,'40.30','2026-02-14 17:13:55'),
|
|
||||||
(149,2,3,'17.80','2026-02-14 17:13:55'),
|
|
||||||
(150,2,1,'0.50','2026-02-14 17:14:57'),
|
|
||||||
(151,2,2,'40.30','2026-02-14 17:14:57'),
|
|
||||||
(152,2,3,'17.80','2026-02-14 17:14:57'),
|
|
||||||
(153,2,1,'1.50','2026-02-14 17:15:58'),
|
|
||||||
(154,2,2,'40.70','2026-02-14 17:15:58'),
|
|
||||||
(155,2,3,'17.80','2026-02-14 17:15:58'),
|
|
||||||
(156,2,1,'1.30','2026-02-14 17:16:59'),
|
|
||||||
(157,2,2,'40.60','2026-02-14 17:16:59'),
|
|
||||||
(158,2,3,'17.80','2026-02-14 17:16:59'),
|
|
||||||
(159,2,1,'1.00','2026-02-14 17:18:01'),
|
|
||||||
(160,2,2,'40.40','2026-02-14 17:18:01'),
|
|
||||||
(161,2,3,'17.80','2026-02-14 17:18:01'),
|
|
||||||
(162,2,1,'0.30','2026-02-14 17:19:02'),
|
|
||||||
(163,2,2,'40.50','2026-02-14 17:19:02'),
|
|
||||||
(164,2,3,'17.80','2026-02-14 17:19:02'),
|
|
||||||
(165,2,1,'0.30','2026-02-14 17:20:03'),
|
|
||||||
(166,2,2,'40.50','2026-02-14 17:20:03'),
|
|
||||||
(167,2,3,'17.80','2026-02-14 17:20:03'),
|
|
||||||
(168,2,1,'0.50','2026-02-14 17:21:05'),
|
|
||||||
(169,2,2,'40.50','2026-02-14 17:21:05'),
|
|
||||||
(170,2,3,'17.80','2026-02-14 17:21:05'),
|
|
||||||
(171,2,1,'4.20','2026-02-14 17:21:56'),
|
|
||||||
(172,2,2,'40.70','2026-02-14 17:21:56'),
|
|
||||||
(173,2,3,'17.80','2026-02-14 17:21:56'),
|
|
||||||
(174,2,1,'2.50','2026-02-14 17:22:06'),
|
|
||||||
(175,2,2,'40.40','2026-02-14 17:22:06'),
|
|
||||||
(176,2,3,'17.80','2026-02-14 17:22:06'),
|
|
||||||
(177,2,1,'0.5','2026-02-14 17:23:07'),
|
|
||||||
(178,2,2,'40.5','2026-02-14 17:23:07'),
|
|
||||||
(179,2,3,'17.8','2026-02-14 17:23:07'),
|
|
||||||
(183,2,1,'2.5','2026-02-14 17:24:09'),
|
|
||||||
(184,2,2,'40.5','2026-02-14 17:24:09'),
|
|
||||||
(185,2,3,'17.8','2026-02-14 17:24:09'),
|
|
||||||
(188,2,1,'0.8','2026-02-14 17:25:10'),
|
|
||||||
(189,2,2,'40.6','2026-02-14 17:25:10'),
|
|
||||||
(190,2,3,'17.8','2026-02-14 17:25:10'),
|
|
||||||
(193,2,1,'3.3','2026-02-14 17:26:12'),
|
|
||||||
(194,2,2,'39.6','2026-02-14 17:26:12'),
|
|
||||||
(195,2,3,'17.8','2026-02-14 17:26:12'),
|
|
||||||
(198,2,1,'0.3','2026-02-14 17:27:13'),
|
|
||||||
(199,2,2,'40.9','2026-02-14 17:27:14'),
|
|
||||||
(200,2,3,'17.8','2026-02-14 17:27:14'),
|
|
||||||
(203,2,1,'1.3','2026-02-14 17:28:15'),
|
|
||||||
(204,2,2,'39.7','2026-02-14 17:28:15'),
|
|
||||||
(205,2,3,'17.8','2026-02-14 17:28:15'),
|
|
||||||
(208,2,1,'1.8','2026-02-14 17:29:17'),
|
|
||||||
(209,2,2,'39.5','2026-02-14 17:29:17'),
|
|
||||||
(210,2,3,'17.8','2026-02-14 17:29:17'),
|
|
||||||
(213,2,1,'0.8','2026-02-14 17:30:18'),
|
|
||||||
(214,2,2,'40.2','2026-02-14 17:30:18'),
|
|
||||||
(215,2,3,'17.8','2026-02-14 17:30:18'),
|
|
||||||
(218,2,1,'0.5','2026-02-14 17:31:20'),
|
|
||||||
(219,2,2,'40.3','2026-02-14 17:31:20'),
|
|
||||||
(220,2,3,'17.8','2026-02-14 17:31:20'),
|
|
||||||
(223,2,1,'0.3','2026-02-14 17:32:21'),
|
|
||||||
(224,2,2,'40.2','2026-02-14 17:32:21'),
|
|
||||||
(225,2,3,'17.8','2026-02-14 17:32:21'),
|
|
||||||
(228,2,1,'0.5','2026-02-14 17:33:23'),
|
|
||||||
(229,2,2,'40.3','2026-02-14 17:33:23'),
|
|
||||||
(230,2,3,'17.8','2026-02-14 17:33:23'),
|
|
||||||
(233,2,1,'0.5','2026-02-14 17:34:24'),
|
|
||||||
(234,2,2,'40.3','2026-02-14 17:34:24'),
|
|
||||||
(235,2,3,'17.8','2026-02-14 17:34:24'),
|
|
||||||
(238,2,1,'1.2','2026-02-14 17:35:26'),
|
|
||||||
(239,2,2,'40.9','2026-02-14 17:35:26'),
|
|
||||||
(240,2,3,'17.8','2026-02-14 17:35:26'),
|
|
||||||
(243,2,1,'0.3','2026-02-14 17:36:27'),
|
|
||||||
(244,2,2,'40.3','2026-02-14 17:36:27'),
|
|
||||||
(245,2,3,'17.8','2026-02-14 17:36:27'),
|
|
||||||
(248,2,1,'0','2026-02-14 17:37:29'),
|
|
||||||
(249,2,2,'40.3','2026-02-14 17:37:29'),
|
|
||||||
(250,2,3,'17.8','2026-02-14 17:37:29'),
|
|
||||||
(253,2,1,'0.5','2026-02-14 17:38:30'),
|
|
||||||
(254,2,2,'40.4','2026-02-14 17:38:30'),
|
|
||||||
(255,2,3,'17.8','2026-02-14 17:38:30'),
|
|
||||||
(258,2,1,'0.5','2026-02-14 17:39:32'),
|
|
||||||
(259,2,2,'40.3','2026-02-14 17:39:32'),
|
|
||||||
(260,2,3,'17.8','2026-02-14 17:39:32'),
|
|
||||||
(263,2,1,'1.7','2026-02-14 17:40:33'),
|
|
||||||
(264,2,2,'40.4','2026-02-14 17:40:33'),
|
|
||||||
(265,2,3,'17.8','2026-02-14 17:40:33'),
|
|
||||||
(268,2,1,'3.5','2026-02-14 17:41:35'),
|
|
||||||
(269,2,2,'39.7','2026-02-14 17:41:35'),
|
|
||||||
(270,2,3,'17.8','2026-02-14 17:41:35'),
|
|
||||||
(273,2,1,'0.5','2026-02-14 17:42:36'),
|
|
||||||
(274,2,2,'40','2026-02-14 17:42:36'),
|
|
||||||
(275,2,3,'17.8','2026-02-14 17:42:36'),
|
|
||||||
(278,2,1,'6.5','2026-02-14 17:43:38'),
|
|
||||||
(279,2,2,'40.5','2026-02-14 17:43:38'),
|
|
||||||
(280,2,3,'17.8','2026-02-14 17:43:38'),
|
|
||||||
(283,2,1,'0.3','2026-02-14 17:44:40'),
|
|
||||||
(284,2,2,'40.6','2026-02-14 17:44:40'),
|
|
||||||
(285,2,3,'17.8','2026-02-14 17:44:40'),
|
|
||||||
(288,2,1,'0.3','2026-02-14 17:45:41'),
|
|
||||||
(289,2,2,'39.5','2026-02-14 17:45:41'),
|
|
||||||
(290,2,3,'17.8','2026-02-14 17:45:41'),
|
|
||||||
(293,2,1,'0.3','2026-02-14 17:46:43'),
|
|
||||||
(294,2,2,'39.2','2026-02-14 17:46:43'),
|
|
||||||
(295,2,3,'17.8','2026-02-14 17:46:43'),
|
|
||||||
(298,2,1,'0.7','2026-02-14 17:47:44'),
|
|
||||||
(299,2,2,'39.8','2026-02-14 17:47:44'),
|
|
||||||
(300,2,3,'17.8','2026-02-14 17:47:44'),
|
|
||||||
(303,2,1,'0.5','2026-02-14 17:48:46'),
|
|
||||||
(304,2,2,'39.9','2026-02-14 17:48:46'),
|
|
||||||
(305,2,3,'17.8','2026-02-14 17:48:46'),
|
|
||||||
(308,2,1,'0.3','2026-02-14 17:49:47'),
|
|
||||||
(309,2,2,'39.8','2026-02-14 17:49:47'),
|
|
||||||
(310,2,3,'17.8','2026-02-14 17:49:47'),
|
|
||||||
(313,2,1,'0.5','2026-02-14 17:50:49'),
|
|
||||||
(314,2,2,'39.9','2026-02-14 17:50:49'),
|
|
||||||
(315,2,3,'17.8','2026-02-14 17:50:49'),
|
|
||||||
(318,2,1,'0.3','2026-02-14 17:51:50'),
|
|
||||||
(319,2,2,'40.1','2026-02-14 17:51:50'),
|
|
||||||
(320,2,3,'17.8','2026-02-14 17:51:50'),
|
|
||||||
(323,2,1,'0','2026-02-14 17:52:52'),
|
|
||||||
(324,2,2,'40.8','2026-02-14 17:52:52'),
|
|
||||||
(325,2,3,'17.8','2026-02-14 17:52:52');
|
|
||||||
/*!40000 ALTER TABLE `server_metrics` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `servers`
|
-- Table structure for table `servers`
|
||||||
--
|
--
|
||||||
|
|
@ -524,18 +206,6 @@ CREATE TABLE `servers` (
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `servers`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `servers` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `servers` DISABLE KEYS */;
|
|
||||||
INSERT INTO `servers` VALUES
|
|
||||||
(1,'Work_PC','',1,'',NULL,'2026-02-03 07:24:02',NULL,0),
|
|
||||||
(2,'tomas','localhost',1,'Main OpenClaw server','2026-02-14 17:52:52','2026-02-14 09:55:18','2026-02-14 10:07:10',0);
|
|
||||||
/*!40000 ALTER TABLE `servers` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `service_alerts`
|
-- Table structure for table `service_alerts`
|
||||||
--
|
--
|
||||||
|
|
@ -555,80 +225,9 @@ CREATE TABLE `service_alerts` (
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_server_service` (`server_id`,`service_name`),
|
KEY `idx_server_service` (`server_id`,`service_name`),
|
||||||
CONSTRAINT `service_alerts_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE
|
CONSTRAINT `service_alerts_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=104 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `service_alerts`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `service_alerts` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `service_alerts` DISABLE KEYS */;
|
|
||||||
INSERT INTO `service_alerts` VALUES
|
|
||||||
(1,2,'apport-autoreport.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(2,2,'apt-daily-upgrade.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(3,2,'apt-daily.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(4,2,'certbot.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(5,2,'cloud-init-local.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(6,2,'dm-event.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(7,2,'dmesg.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(8,2,'dpkg-db-backup.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(9,2,'e2scrub_all.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(10,2,'e2scrub_reap.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(11,2,'emergency.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(12,2,'fstrim.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(13,2,'fwupd-refresh.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(14,2,'getty-static.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(15,2,'grub-common.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(16,2,'grub-initrd-fallback.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(17,2,'initrd-cleanup.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(18,2,'initrd-parse-etc.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(19,2,'initrd-switch-root.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(20,2,'initrd-udevadm-cleanup-db.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(21,2,'iscsid.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(22,2,'ldconfig.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(23,2,'logrotate.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(24,2,'lvm2-lvmpolld.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(25,2,'man-db.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(26,2,'modprobe@configfs.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(27,2,'modprobe@dm_mod.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(28,2,'modprobe@drm.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(29,2,'modprobe@efi_pstore.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(30,2,'modprobe@fuse.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(31,2,'modprobe@loop.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(32,2,'motd-news.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(33,2,'netplan-ovs-cleanup.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(34,2,'networkd-dispatcher.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(35,2,'nftables.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(36,2,'open-iscsi.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(37,2,'open-vm-tools.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(38,2,'phpsessionclean.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(39,2,'plymouth-start.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(40,2,'plymouth-switch-root.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(41,2,'pollinate.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(42,2,'rc-local.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(43,2,'rescue.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(44,2,'secureboot-db.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(45,2,'snapd.autoimport.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(46,2,'snapd.core-fixup.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(47,2,'snapd.failure.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(48,2,'snapd.recovery-chooser-trigger.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(49,2,'snapd.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(50,2,'snapd.snap-repair.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(51,2,'snapd.system-shutdown.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(52,2,'sysstat-collect.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(53,2,'sysstat-summary.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(54,2,'systemd-ask-password-console.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(55,2,'systemd-ask-password-plymouth.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(56,2,'systemd-ask-password-wall.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(57,2,'systemd-battery-check.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(58,2,'systemd-bsod.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(59,2,'systemd-firstboot.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(60,2,'systemd-fsck-root.service','stopped','critical',0,'2026-02-14 10:05:13',NULL),
|
|
||||||
(61,2,'server-monitor-agent.service','stopped','critical',0,'2026-02-14 10:06:19',NULL);
|
|
||||||
/*!40000 ALTER TABLE `service_alerts` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `service_status`
|
-- Table structure for table `service_status`
|
||||||
--
|
--
|
||||||
|
|
@ -639,7 +238,7 @@ DROP TABLE IF EXISTS `service_status`;
|
||||||
CREATE TABLE `service_status` (
|
CREATE TABLE `service_status` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`server_id` int(11) NOT NULL,
|
`server_id` int(11) NOT NULL,
|
||||||
`service_name` varchar(100) NOT NULL,
|
`service_name` varchar(255) NOT NULL,
|
||||||
`status` enum('running','stopped','unknown') NOT NULL,
|
`status` enum('running','stopped','unknown') NOT NULL,
|
||||||
`load_state` varchar(50) DEFAULT NULL,
|
`load_state` varchar(50) DEFAULT NULL,
|
||||||
`active_state` varchar(50) DEFAULT NULL,
|
`active_state` varchar(50) DEFAULT NULL,
|
||||||
|
|
@ -650,115 +249,9 @@ CREATE TABLE `service_status` (
|
||||||
UNIQUE KEY `uk_server_service` (`server_id`,`service_name`),
|
UNIQUE KEY `uk_server_service` (`server_id`,`service_name`),
|
||||||
KEY `idx_server_updated` (`server_id`,`updated_at`),
|
KEY `idx_server_updated` (`server_id`,`updated_at`),
|
||||||
CONSTRAINT `service_status_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE
|
CONSTRAINT `service_status_ibfk_1` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=7731 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=14919756 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `service_status`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `service_status` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `service_status` DISABLE KEYS */;
|
|
||||||
INSERT INTO `service_status` VALUES
|
|
||||||
(1,2,'ssh','running','loaded','active','running','2026-02-14 10:05:07','2026-02-14 10:05:07'),
|
|
||||||
(2,2,'apparmor.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(3,2,'apport-autoreport.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(4,2,'apport.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(5,2,'apt-daily-upgrade.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(6,2,'apt-daily.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(7,2,'●','unknown','rbdmap.service','not-found','inactive','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(8,2,'blk-availability.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(9,2,'certbot.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(10,2,'cloud-init-local.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(13,2,'console-setup.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(14,2,'containerd.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(15,2,'cron.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(16,2,'dbus.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(18,2,'dm-event.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(19,2,'dmesg.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(20,2,'docker.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(21,2,'dpkg-db-backup.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(22,2,'e2scrub_all.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(23,2,'e2scrub_reap.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(24,2,'emergency.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(25,2,'fail2ban.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(27,2,'finalrd.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(29,2,'fstrim.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(30,2,'fwupd-refresh.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(31,2,'fwupd.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(32,2,'getty-static.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(33,2,'getty@tty1.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(34,2,'grub-common.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(35,2,'grub-initrd-fallback.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(37,2,'initrd-cleanup.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(38,2,'initrd-parse-etc.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(39,2,'initrd-switch-root.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(40,2,'initrd-udevadm-cleanup-db.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(45,2,'iscsid.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(47,2,'keyboard-setup.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(48,2,'kmod-static-nodes.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(49,2,'ldconfig.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(50,2,'logrotate.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(52,2,'lvm2-lvmpolld.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(53,2,'lvm2-monitor.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(55,2,'man-db.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(56,2,'mariadb.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(57,2,'ModemManager.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(58,2,'modprobe@configfs.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(59,2,'modprobe@dm_mod.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(60,2,'modprobe@drm.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(61,2,'modprobe@efi_pstore.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(62,2,'modprobe@fuse.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(63,2,'modprobe@loop.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(64,2,'mon-server.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(67,2,'motd-news.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(68,2,'multipathd.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(69,2,'netplan-ovs-cleanup.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(70,2,'networkd-dispatcher.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(72,2,'nftables.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(73,2,'nginx.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(74,2,'open-iscsi.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(75,2,'open-vm-tools.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(77,2,'php8.3-fpm.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(78,2,'phpsessionclean.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(79,2,'plymouth-quit-wait.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(80,2,'plymouth-quit.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(81,2,'plymouth-read-write.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(82,2,'plymouth-start.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(83,2,'plymouth-switch-root.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(84,2,'polkit.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(85,2,'pollinate.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(87,2,'rc-local.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(88,2,'rescue.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(89,2,'rsyslog.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(90,2,'secureboot-db.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(91,2,'server-monitor-agent.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(92,2,'setvtrgb.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(93,2,'snapd.apparmor.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(94,2,'snapd.autoimport.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(95,2,'snapd.core-fixup.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(96,2,'snapd.failure.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(97,2,'snapd.recovery-chooser-trigger.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(98,2,'snapd.seeded.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(99,2,'snapd.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(100,2,'snapd.snap-repair.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(101,2,'snapd.system-shutdown.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(102,2,'ssh.service','running','loaded','active','running','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(103,2,'sysstat-collect.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(104,2,'sysstat-summary.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(105,2,'sysstat.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(106,2,'systemd-ask-password-console.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(107,2,'systemd-ask-password-plymouth.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(108,2,'systemd-ask-password-wall.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(109,2,'systemd-battery-check.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(110,2,'systemd-binfmt.service','running','loaded','active','exited','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(111,2,'systemd-bsod.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(112,2,'systemd-firstboot.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(113,2,'systemd-fsck-root.service','stopped','loaded','inactive','dead','2026-02-14 17:52:52','2026-02-14 10:05:13'),
|
|
||||||
(338,2,'test','running','','','','2026-02-14 10:07:10','2026-02-14 10:07:10');
|
|
||||||
/*!40000 ALTER TABLE `service_status` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `user_notification_settings`
|
-- Table structure for table `user_notification_settings`
|
||||||
--
|
--
|
||||||
|
|
@ -771,21 +264,15 @@ CREATE TABLE `user_notification_settings` (
|
||||||
`user_id` int(11) NOT NULL,
|
`user_id` int(11) NOT NULL,
|
||||||
`telegram_chat_id` varchar(50) DEFAULT NULL,
|
`telegram_chat_id` varchar(50) DEFAULT NULL,
|
||||||
`email_for_alerts` varchar(100) DEFAULT NULL,
|
`email_for_alerts` varchar(100) DEFAULT NULL,
|
||||||
|
`enabled_notifications` tinyint(1) DEFAULT 1,
|
||||||
|
`notify_on_warning` tinyint(1) DEFAULT 1,
|
||||||
|
`notify_on_critical` tinyint(1) DEFAULT 1,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `user_id` (`user_id`),
|
KEY `user_id` (`user_id`),
|
||||||
CONSTRAINT `user_notification_settings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
CONSTRAINT `user_notification_settings_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `user_notification_settings`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `user_notification_settings` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `user_notification_settings` DISABLE KEYS */;
|
|
||||||
/*!40000 ALTER TABLE `user_notification_settings` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `users`
|
-- Table structure for table `users`
|
||||||
--
|
--
|
||||||
|
|
@ -802,20 +289,8 @@ CREATE TABLE `users` (
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `username` (`username`)
|
UNIQUE KEY `username` (`username`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `users`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `users` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
|
|
||||||
INSERT INTO `users` VALUES
|
|
||||||
(1,'admin','$2y$10$5PhDSHiF1J6yxcEldOsluOSmUYaO1bWa7swFmfmP/Slj.HJOh5t2O','admin@example.com','admin','2026-02-03 06:58:49'),
|
|
||||||
(2,'jarvis','$2y$10$vtqwvVd4Sd/RqZI33eW94Oxk8k343hUbHkIJEkjP18u5z6Ugj8VSy','jarvis@mon.mirv.top','user','2026-02-14 09:51:02');
|
|
||||||
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||||
|
|
||||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
|
@ -826,4 +301,85 @@ UNLOCK TABLES;
|
||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
-- Dump completed on 2026-02-14 17:53:04
|
-- Dump completed on 2026-04-13 0:54:46
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Примеры данных для демонстрации и тестирования
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Пользователи (пароль для обоих: admin123)
|
||||||
|
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role`, `is_active`, `created_at`) VALUES
|
||||||
|
(1, 'admin', 'admin@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 1, NOW()),
|
||||||
|
(2, 'operator', 'operator@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'operator', 1, NOW());
|
||||||
|
|
||||||
|
-- Группы серверов
|
||||||
|
INSERT INTO `server_groups` (`id`, `name`, `description`, `icon`, `color`, `sort_order`) VALUES
|
||||||
|
(1, 'Production', 'Продакшн серверы', 'fa-server', '#dc3545', 1),
|
||||||
|
(2, 'Staging', 'Тестовые серверы', 'fa-flask', '#ffc107', 2),
|
||||||
|
(3, 'Database', 'Серверы баз данных', 'fa-database', '#0dcaf0', 3);
|
||||||
|
|
||||||
|
-- Серверы
|
||||||
|
INSERT INTO `servers` (`id`, `name`, `address`, `description`, `group_id`, `is_active`, `created_at`) VALUES
|
||||||
|
(1, 'web-server-01', '192.168.1.10', 'Основной веб-сервер', 1, 1, NOW()),
|
||||||
|
(2, 'web-server-02', '192.168.1.11', 'Веб-сервер (резервный)', 1, 1, NOW()),
|
||||||
|
(3, 'db-server-01', '192.168.1.20', 'PostgreSQL основной', 3, 1, NOW()),
|
||||||
|
(4, 'staging-app', '192.168.1.30', 'Staging окружение', 2, 1, NOW());
|
||||||
|
|
||||||
|
-- Настройки агентов
|
||||||
|
INSERT INTO `agent_configs` (`id`, `server_id`, `interval_seconds`, `monitor_services`, `enabled`) VALUES
|
||||||
|
(1, 1, 60, '["nginx", "php-fpm", "sshd"]', 1),
|
||||||
|
(2, 2, 60, '["nginx", "php-fpm"]', 1),
|
||||||
|
(3, 3, 30, '["postgresql", "sshd"]', 1),
|
||||||
|
(4, 4, 120, '["nginx"]', 1);
|
||||||
|
|
||||||
|
-- Названия метрик
|
||||||
|
INSERT INTO `metric_names` (`id`, `name`, `description`, `unit`) VALUES
|
||||||
|
(1, 'cpu_load', 'Загрузка CPU', '%'),
|
||||||
|
(2, 'ram_used', 'Использование RAM', '%'),
|
||||||
|
(3, 'disk_used', 'Использование диска', '%'),
|
||||||
|
(4, 'network_rx', 'Входящий трафик', 'Mbps'),
|
||||||
|
(5, 'network_tx', 'Исходящий трафик', 'Mbps'),
|
||||||
|
(6, 'disk_read', 'Чтение диска', 'MB/s'),
|
||||||
|
(7, 'disk_write', 'Запись диска', 'MB/s');
|
||||||
|
|
||||||
|
-- Примеры метрик (последние сутки)
|
||||||
|
INSERT INTO `server_metrics` (`server_id`, `metric_name`, `value`, `time_bucket`) VALUES
|
||||||
|
(1, 'cpu_load', 45.2, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(1, 'cpu_load', 62.8, NOW() - INTERVAL 50 MINUTE),
|
||||||
|
(1, 'cpu_load', 78.1, NOW() - INTERVAL 40 MINUTE),
|
||||||
|
(1, 'cpu_load', 55.3, NOW() - INTERVAL 30 MINUTE),
|
||||||
|
(1, 'cpu_load', 41.7, NOW() - INTERVAL 20 MINUTE),
|
||||||
|
(1, 'cpu_load', 38.9, NOW() - INTERVAL 10 MINUTE),
|
||||||
|
(1, 'ram_used', 67.5, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(1, 'ram_used', 68.2, NOW() - INTERVAL 30 MINUTE),
|
||||||
|
(1, 'ram_used', 69.1, NOW() - INTERVAL 10 MINUTE),
|
||||||
|
(1, 'disk_used', 72.3, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(3, 'cpu_load', 23.4, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(3, 'cpu_load', 31.2, NOW() - INTERVAL 30 MINUTE),
|
||||||
|
(3, 'ram_used', 82.1, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(3, 'ram_used', 83.5, NOW() - INTERVAL 30 MINUTE);
|
||||||
|
|
||||||
|
-- Пороги
|
||||||
|
INSERT INTO `metric_thresholds` (`server_id`, `metric_name`, `warning_threshold`, `critical_threshold`, `duration_minutes`) VALUES
|
||||||
|
(1, 'cpu_load', 80.0, 90.0, 5),
|
||||||
|
(1, 'ram_used', 85.0, 95.0, 10),
|
||||||
|
(3, 'cpu_load', 75.0, 85.0, 5),
|
||||||
|
(3, 'ram_used', 80.0, 90.0, 5);
|
||||||
|
|
||||||
|
-- Алерты
|
||||||
|
INSERT INTO `alerts` (`id`, `server_id`, `metric_name`, `value`, `severity`, `resolved`, `created_at`, `resolved_at`) VALUES
|
||||||
|
(1, 3, 'ram_used', 82.10, 'warning', 1, NOW() - INTERVAL 2 HOUR, NOW() - INTERVAL 1 HOUR),
|
||||||
|
(2, 1, 'cpu_load', 91.50, 'critical', 0, NOW() - INTERVAL 30 MINUTE, NULL);
|
||||||
|
|
||||||
|
-- Статусы сервисов
|
||||||
|
INSERT INTO `service_status` (`server_id`, `service_name`, `load_state`, `active_state`, `last_check`, `is_monitored`) VALUES
|
||||||
|
(1, 'nginx', 'loaded', 'active', NOW(), 1),
|
||||||
|
(1, 'php8.2-fpm', 'loaded', 'active', NOW(), 1),
|
||||||
|
(1, 'sshd', 'loaded', 'active', NOW(), 1),
|
||||||
|
(3, 'postgresql', 'loaded', 'active', NOW(), 1),
|
||||||
|
(3, 'sshd', 'loaded', 'active', NOW(), 1);
|
||||||
|
|
||||||
|
-- Глобальные настройки уведомлений
|
||||||
|
INSERT INTO `global_notification_settings` (`id`, `smtp_host`, `smtp_port`, `smtp_username`, `smtp_from_email`, `telegram_bot_token`, `telegram_chat_id`, `telegram_proxy`, `email_enabled`, `telegram_enabled`) VALUES
|
||||||
|
(1, 'smtp.example.com', 587, 'noreply@example.com', 'noreply@example.com', '123456789:AAExampleBotToken', '-1001234567890', '', 0, 0);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -129,7 +129,7 @@ $dashboardGroup = $app->group('', function ($group) use ($twig) {
|
||||||
$stats = $serverModel->getStats();
|
$stats = $serverModel->getStats();
|
||||||
|
|
||||||
// Get servers with latest metrics
|
// Get servers with latest metrics
|
||||||
$servers = $serverModel->getAll();
|
$servers = $serverModel->getServersWithStatus();
|
||||||
|
|
||||||
$templateData = [
|
$templateData = [
|
||||||
'title' => 'Дашборд мониторинга',
|
'title' => 'Дашборд мониторинга',
|
||||||
|
|
@ -161,6 +161,11 @@ $groupsGroup = $app->group('/groups', function ($group) use ($groupController) {
|
||||||
$group->get('/{id}', [$groupController, 'show']);
|
$group->get('/{id}', [$groupController, 'show']);
|
||||||
})->add($csrfMiddleware)->add(AuthMiddleware::class);
|
})->add($csrfMiddleware)->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Redirect old /server/{id} to /servers/{id}
|
||||||
|
$app->get("/server/{id}", function ($request, $response, $args) {
|
||||||
|
return $response->withHeader("Location", "/servers/" . $args["id"])->withStatus(301);
|
||||||
|
})->add(AuthMiddleware::class);
|
||||||
|
|
||||||
// Routes for servers (protected with auth middleware and csrf)
|
// Routes for servers (protected with auth middleware and csrf)
|
||||||
$serversGroup = $app->group('/servers', function ($group) use ($serverController, $serverDetailController) {
|
$serversGroup = $app->group('/servers', function ($group) use ($serverController, $serverDetailController) {
|
||||||
$group->get('', [$serverController, 'index']);
|
$group->get('', [$serverController, 'index']);
|
||||||
|
|
@ -186,7 +191,11 @@ $alertsGroup = $app->group('/alerts', function ($group) use ($alertController) {
|
||||||
// Admin routes (protected with auth middleware and csrf)
|
// Admin routes (protected with auth middleware and csrf)
|
||||||
$adminGroup = $app->group('/admin', function ($group) use ($adminController) {
|
$adminGroup = $app->group('/admin', function ($group) use ($adminController) {
|
||||||
$group->get('/users', [$adminController, 'usersList']);
|
$group->get('/users', [$adminController, 'usersList']);
|
||||||
|
$group->post("/users/save", [$adminController, "saveUser"]);
|
||||||
|
$group->get("/users/{id}/delete", [$adminController, "deleteUser"]);
|
||||||
$group->get('/notifications', [$adminController, 'notificationSettings']);
|
$group->get('/notifications', [$adminController, 'notificationSettings']);
|
||||||
|
$group->post("/notifications/save", [$adminController, "saveNotificationSettings"]);
|
||||||
|
$group->get("/notifications/test", [$adminController, "testNotification"]);
|
||||||
})->add($csrfMiddleware)->add(AuthMiddleware::class);
|
})->add($csrfMiddleware)->add(AuthMiddleware::class);
|
||||||
|
|
||||||
// API route for agents (public, no auth middleware, no csrf)
|
// API route for agents (public, no auth middleware, no csrf)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Models\Model;
|
use App\Models\Model;
|
||||||
|
use App\Services\NotificationService;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Slim\Views\Twig;
|
use Slim\Views\Twig;
|
||||||
|
|
@ -11,43 +12,259 @@ use Slim\Views\Twig;
|
||||||
class AdminController extends Model
|
class AdminController extends Model
|
||||||
{
|
{
|
||||||
private $twig;
|
private $twig;
|
||||||
|
private $notificationService;
|
||||||
|
|
||||||
public function __construct(Twig $twig)
|
public function __construct(Twig $twig)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
|
$this->notificationService = new NotificationService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== ПОЛЬЗОВАТЕЛИ ====================
|
||||||
|
|
||||||
public function usersList(Request $request, Response $response, $args)
|
public function usersList(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
// Только для администраторов
|
|
||||||
if ($_SESSION['role'] !== 'admin') {
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
return $response->withHeader('Location', '/')->withStatus(302);
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare("SELECT id, username, email, role, created_at FROM users ORDER BY created_at DESC");
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT u.id, u.username, u.email, u.role, u.created_at,
|
||||||
|
uns.telegram_chat_id, uns.email_for_alerts
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN user_notification_settings uns ON u.id = uns.user_id
|
||||||
|
ORDER BY u.created_at DESC
|
||||||
|
");
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$users = $stmt->fetchAll();
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
$templateData = [
|
return $this->twig->render($response, 'admin/users.twig', [
|
||||||
'title' => 'Управление пользователями',
|
'title' => 'Управление пользователями',
|
||||||
'users' => $users
|
'users' => $users
|
||||||
];
|
]);
|
||||||
|
|
||||||
return $this->twig->render($response, 'admin/users.twig', $templateData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notificationSettings(Request $request, Response $response, $args)
|
public function saveUser(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
// Только для администраторов
|
|
||||||
if ($_SESSION['role'] !== 'admin') {
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
return $response->withHeader('Location', '/')->withStatus(302);
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
$templateData = [
|
$data = $request->getParsedBody();
|
||||||
'title' => 'Настройки уведомлений'
|
$userId = $data['user_id'] ?? null;
|
||||||
];
|
$username = trim($data['username'] ?? '');
|
||||||
|
$email = trim($data['email'] ?? '');
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
$role = in_array($data['role'], ['admin', 'user']) ? $data['role'] : 'user';
|
||||||
|
$telegramChatId = trim($data['telegram_chat_id'] ?? '');
|
||||||
|
$emailForAlerts = trim($data['email_for_alerts'] ?? '');
|
||||||
|
|
||||||
return $this->twig->render($response, 'admin/notifications.twig', $templateData);
|
if (empty($username)) {
|
||||||
|
$_SESSION['flash_message'] = 'Имя пользователя обязательно';
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($userId) {
|
||||||
|
// Редактирование
|
||||||
|
if (!empty($password)) {
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE users SET username = :username, email = :email, password_hash = :password_hash, role = :role
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':username' => $username,
|
||||||
|
':email' => $email,
|
||||||
|
':password_hash' => $passwordHash,
|
||||||
|
':role' => $role,
|
||||||
|
':id' => $userId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE users SET username = :username, email = :email, role = :role WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':username' => $username,
|
||||||
|
':email' => $email,
|
||||||
|
':role' => $role,
|
||||||
|
':id' => $userId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем настройки уведомлений
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO user_notification_settings (user_id, telegram_chat_id, email_for_alerts)
|
||||||
|
VALUES (:user_id, :telegram_chat_id, :email_for_alerts)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
telegram_chat_id = VALUES(telegram_chat_id),
|
||||||
|
email_for_alerts = VALUES(email_for_alerts)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':user_id' => $userId,
|
||||||
|
':telegram_chat_id' => $telegramChatId,
|
||||||
|
':email_for_alerts' => $emailForAlerts
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = "Пользователь «{$username}» обновлён";
|
||||||
|
} else {
|
||||||
|
// Создание
|
||||||
|
if (empty($password)) {
|
||||||
|
$_SESSION['flash_message'] = 'Пароль обязателен при создании пользователя';
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем уникальность имени
|
||||||
|
$stmt = $this->pdo->prepare("SELECT id FROM users WHERE username = :username");
|
||||||
|
$stmt->execute([':username' => $username]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$_SESSION['flash_message'] = "Пользователь «{$username}» уже существует";
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO users (username, email, password_hash, role)
|
||||||
|
VALUES (:username, :email, :password_hash, :role)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':username' => $username,
|
||||||
|
':email' => $email,
|
||||||
|
':password_hash' => $passwordHash,
|
||||||
|
':role' => $role
|
||||||
|
]);
|
||||||
|
$newUserId = $this->pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Создаём настройки уведомлений
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO user_notification_settings (user_id, telegram_chat_id, email_for_alerts)
|
||||||
|
VALUES (:user_id, :telegram_chat_id, :email_for_alerts)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':user_id' => $newUserId,
|
||||||
|
':telegram_chat_id' => $telegramChatId,
|
||||||
|
':email_for_alerts' => $emailForAlerts
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = "Пользователь «{$username}» создан";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdo->commit();
|
||||||
|
$_SESSION['flash_message'] = $message;
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
$_SESSION['flash_message'] = 'Ошибка: ' . $e->getMessage();
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteUser(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $args['id'];
|
||||||
|
|
||||||
|
// Не даём удалить себя
|
||||||
|
if ($userId == $_SESSION['user_id']) {
|
||||||
|
$_SESSION['flash_message'] = 'Нельзя удалить себя';
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare("SELECT username FROM users WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $userId]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$_SESSION['flash_message'] = 'Пользователь не найден';
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM users WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $userId]);
|
||||||
|
|
||||||
|
$_SESSION['flash_message'] = "Пользователь «{$user['username']}» удалён";
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$_SESSION['flash_message'] = 'Ошибка удаления: ' . $e->getMessage();
|
||||||
|
$_SESSION['flash_type'] = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/admin/users')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== НАСТРОЙКИ УВЕДОМЛЕНИЙ ====================
|
||||||
|
|
||||||
|
public function notificationSettings(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->notificationService->getSettings();
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'admin/notifications.twig', [
|
||||||
|
'title' => 'Настройки уведомлений',
|
||||||
|
'settings' => $settings
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveNotificationSettings(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
$this->notificationService->saveSettings($data);
|
||||||
|
|
||||||
|
$_SESSION['flash_message'] = 'Настройки уведомлений сохранены';
|
||||||
|
$_SESSION['flash_type'] = 'success';
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/admin/notifications')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotification(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $this->notificationService->getSettings();
|
||||||
|
$results = $this->notificationService->sendTestNotification(
|
||||||
|
$settings['smtp_from_email'],
|
||||||
|
$settings['telegram_chat_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
$status = 'success';
|
||||||
|
$messages = [];
|
||||||
|
foreach ($results as $channel => $result) {
|
||||||
|
if ($result['success']) {
|
||||||
|
$messages[] = "{$channel}: ✅ " . $result['message'];
|
||||||
|
} else {
|
||||||
|
$messages[] = "{$channel}: ❌ " . $result['error'];
|
||||||
|
$status = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['flash_message'] = implode("\n", $messages);
|
||||||
|
$_SESSION['flash_type'] = $status;
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/admin/notifications')->withStatus(302);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,21 @@
|
||||||
namespace App\Controllers\Api;
|
namespace App\Controllers\Api;
|
||||||
|
|
||||||
use App\Models\Model;
|
use App\Models\Model;
|
||||||
|
use App\Services\NotificationService;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Config\DatabaseConfig;
|
use Config\DatabaseConfig;
|
||||||
|
|
||||||
class MetricsController extends Model
|
class MetricsController extends Model
|
||||||
{
|
{
|
||||||
|
private $notificationService;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->notificationService = new NotificationService();
|
||||||
|
}
|
||||||
|
|
||||||
public function collectMetrics(Request $request, Response $response, $args)
|
public function collectMetrics(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
$input = json_decode($request->getBody(), true);
|
$input = json_decode($request->getBody(), true);
|
||||||
|
|
@ -40,6 +49,7 @@ class MetricsController extends Model
|
||||||
}
|
}
|
||||||
|
|
||||||
$serverId = $tokenInfo['server_id'];
|
$serverId = $tokenInfo['server_id'];
|
||||||
|
$serverName = $tokenInfo['server_name'];
|
||||||
|
|
||||||
// Обновляем время последних метрик для сервера
|
// Обновляем время последних метрик для сервера
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
|
|
@ -71,8 +81,8 @@ class MetricsController extends Model
|
||||||
':value' => $value
|
':value' => $value
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Проверяем пороги
|
// Проверяем пороги и отправляем уведомления
|
||||||
$this->checkThresholds($serverId, $metricId, $value, $metricName);
|
$this->checkThresholds($serverId, $metricId, $value, $metricName, $serverName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,9 +121,9 @@ class MetricsController extends Model
|
||||||
':sub_state' => $subState
|
':sub_state' => $subState
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Если сервис остановлен и включено его мониторинг - создаем алерт
|
// Если сервис остановлен - создаем алерт
|
||||||
if ($serviceStatus === 'stopped') {
|
if ($serviceStatus === 'stopped') {
|
||||||
$this->createServiceAlert($serverId, $serviceName, $serviceStatus);
|
$this->createServiceAlert($serverId, $serviceName, $serviceStatus, $serverName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,11 +147,103 @@ class MetricsController extends Model
|
||||||
return $response->withStatus(200);
|
return $response->withStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkThresholds($serverId, $metricId, $value, $metricName, $serverName)
|
||||||
|
{
|
||||||
|
// Получаем пороговые значения для этой метрики на этом сервере
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT warning_threshold, critical_threshold
|
||||||
|
FROM metric_thresholds
|
||||||
|
WHERE server_id = :server_id AND metric_name_id = :metric_name_id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':metric_name_id' => $metricId
|
||||||
|
]);
|
||||||
|
$thresholds = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($thresholds) {
|
||||||
|
$warningThreshold = $thresholds['warning_threshold'];
|
||||||
|
$criticalThreshold = $thresholds['critical_threshold'];
|
||||||
|
|
||||||
|
$severity = null;
|
||||||
|
$threshold = null;
|
||||||
|
if ($criticalThreshold && $value >= $criticalThreshold) {
|
||||||
|
$severity = 'critical';
|
||||||
|
$threshold = $criticalThreshold;
|
||||||
|
} elseif ($warningThreshold && $value >= $warningThreshold) {
|
||||||
|
$severity = 'warning';
|
||||||
|
$threshold = $warningThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($severity) {
|
||||||
|
// Создаем алерт
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO alerts (server_id, metric_name, value, severity)
|
||||||
|
VALUES (:server_id, :metric_name, :value, :severity)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':metric_name' => $metricName,
|
||||||
|
':value' => $value,
|
||||||
|
':severity' => $severity
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Отправляем уведомление
|
||||||
|
$this->notificationService->sendAlertNotification(
|
||||||
|
$serverName,
|
||||||
|
$metricName,
|
||||||
|
$value,
|
||||||
|
$severity,
|
||||||
|
$threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createServiceAlert($serverId, $serviceName, $status, $serverName)
|
||||||
|
{
|
||||||
|
// Проверяем есть ли уже неразрешенный алерт для этого сервиса
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT id FROM service_alerts
|
||||||
|
WHERE server_id = :server_id AND service_name = :service_name AND resolved = FALSE
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':service_name' => $serviceName
|
||||||
|
]);
|
||||||
|
|
||||||
|
$existingAlert = $stmt->fetch();
|
||||||
|
|
||||||
|
// Если алерта нет - создаем новый и отправляем уведомление
|
||||||
|
if (!$existingAlert) {
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO service_alerts (server_id, service_name, status, severity)
|
||||||
|
VALUES (:server_id, :service_name, :status, 'critical')
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':service_name' => $serviceName,
|
||||||
|
':status' => $status
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Отправляем уведомление о остановке сервиса
|
||||||
|
$this->notificationService->sendAlertNotification(
|
||||||
|
$serverName,
|
||||||
|
"Сервис: {$serviceName}",
|
||||||
|
$status,
|
||||||
|
'critical',
|
||||||
|
'running'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getServices(Request $request, Response $response, $args)
|
public function getServices(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
$serverId = $args['id'];
|
$serverId = $args['id'];
|
||||||
|
|
||||||
// Получаем список сервисов
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT service_name, status, load_state, active_state, sub_state, updated_at
|
SELECT service_name, status, load_state, active_state, sub_state, updated_at
|
||||||
FROM service_status
|
FROM service_status
|
||||||
|
|
@ -164,10 +266,12 @@ class MetricsController extends Model
|
||||||
return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Time parameter required']));
|
return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Time parameter required']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пытаемся распознать различные форматы времени
|
|
||||||
$timestamp = strtotime($timeParam);
|
$timestamp = strtotime($timeParam);
|
||||||
|
// Парсинг формата d.m H:i (12.04 07:48)
|
||||||
|
if ($timestamp === false && preg_match("/^(\d{1,2})\.(\d{2}) (\d{1,2}):(\d{2})$/", $timeParam, $m)) {
|
||||||
|
$timestamp = strtotime(date("Y") . "-" . $m[2] . "-" . $m[1] . " " . $m[3] . ":" . $m[4] . ":00");
|
||||||
|
}
|
||||||
|
|
||||||
// Если время только в формате HH:MM, добавляем сегодняшнюю дату
|
|
||||||
if ($timestamp === false && preg_match('/^\d{1,2}:\d{2}$/', $timeParam)) {
|
if ($timestamp === false && preg_match('/^\d{1,2}:\d{2}$/', $timeParam)) {
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
$timestamp = strtotime($today . ' ' . $timeParam);
|
$timestamp = strtotime($today . ' ' . $timeParam);
|
||||||
|
|
@ -179,54 +283,28 @@ class MetricsController extends Model
|
||||||
|
|
||||||
$time = date('Y-m-d H:i:s', $timestamp);
|
$time = date('Y-m-d H:i:s', $timestamp);
|
||||||
|
|
||||||
// Получаем топ-процессы CPU для указанного времени
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT value
|
SELECT value FROM server_metrics sm
|
||||||
FROM server_metrics sm
|
|
||||||
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
||||||
WHERE sm.server_id = :server_id
|
WHERE sm.server_id = :server_id AND mn.name = 'top_cpu_proc'
|
||||||
AND mn.name = 'top_cpu_proc'
|
|
||||||
AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND)
|
AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND)
|
||||||
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3))
|
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1
|
||||||
LIMIT 1
|
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]);
|
||||||
':server_id' => $serverId,
|
|
||||||
':time1' => $time,
|
|
||||||
':time2' => $time,
|
|
||||||
':time3' => $time
|
|
||||||
]);
|
|
||||||
$topCpuResult = $stmt->fetch();
|
$topCpuResult = $stmt->fetch();
|
||||||
|
|
||||||
// Получаем топ-процессы RAM для указанного времени
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT value
|
SELECT value FROM server_metrics sm
|
||||||
FROM server_metrics sm
|
|
||||||
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
||||||
WHERE sm.server_id = :server_id
|
WHERE sm.server_id = :server_id AND mn.name = 'top_ram_proc'
|
||||||
AND mn.name = 'top_ram_proc'
|
|
||||||
AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND)
|
AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND)
|
||||||
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3))
|
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1
|
||||||
LIMIT 1
|
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]);
|
||||||
':server_id' => $serverId,
|
|
||||||
':time1' => $time,
|
|
||||||
':time2' => $time,
|
|
||||||
':time3' => $time
|
|
||||||
]);
|
|
||||||
$topRamResult = $stmt->fetch();
|
$topRamResult = $stmt->fetch();
|
||||||
|
|
||||||
$topCpu = [];
|
$topCpu = $topCpuResult ? json_decode($topCpuResult['value'], true) : [];
|
||||||
$topRam = [];
|
$topRam = $topRamResult ? json_decode($topRamResult['value'], true) : [];
|
||||||
|
|
||||||
if ($topCpuResult && !empty($topCpuResult['value'])) {
|
|
||||||
$topCpu = json_decode($topCpuResult['value'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($topRamResult && !empty($topRamResult['value'])) {
|
|
||||||
$topRam = json_decode($topRamResult['value'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$response->getBody()->write(json_encode([
|
$response->getBody()->write(json_encode([
|
||||||
'top_cpu' => $topCpu,
|
'top_cpu' => $topCpu,
|
||||||
|
|
@ -268,87 +346,12 @@ class MetricsController extends Model
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$response->getBody()->write(json_encode([
|
||||||
'server_id' => (int)$serverId,
|
'server_id' => (int)$serverId,
|
||||||
'from' => $from,
|
'from' => $from,
|
||||||
'to' => $to,
|
'to' => $to,
|
||||||
'points_count' => count($metrics),
|
|
||||||
'metrics' => $grouped
|
'metrics' => $grouped
|
||||||
];
|
]));
|
||||||
|
|
||||||
$response->getBody()->write(json_encode($data));
|
|
||||||
return $response->withHeader('Content-Type', 'application/json');
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkThresholds($serverId, $metricId, $value, $metricName)
|
|
||||||
{
|
|
||||||
// Получаем пороговые значения для этой метрики на этом сервере
|
|
||||||
$stmt = $this->pdo->prepare("
|
|
||||||
SELECT warning_threshold, critical_threshold
|
|
||||||
FROM metric_thresholds
|
|
||||||
WHERE server_id = :server_id AND metric_name_id = :metric_name_id
|
|
||||||
");
|
|
||||||
$stmt->execute([
|
|
||||||
':server_id' => $serverId,
|
|
||||||
':metric_name_id' => $metricId
|
|
||||||
]);
|
|
||||||
$thresholds = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($thresholds) {
|
|
||||||
$warningThreshold = $thresholds['warning_threshold'];
|
|
||||||
$criticalThreshold = $thresholds['critical_threshold'];
|
|
||||||
|
|
||||||
$severity = null;
|
|
||||||
if ($criticalThreshold && $value >= $criticalThreshold) {
|
|
||||||
$severity = 'critical';
|
|
||||||
} elseif ($warningThreshold && $value >= $warningThreshold) {
|
|
||||||
$severity = 'warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($severity) {
|
|
||||||
// Создаем алерт
|
|
||||||
$stmt = $this->pdo->prepare("
|
|
||||||
INSERT INTO alerts (server_id, metric_name, value, severity)
|
|
||||||
VALUES (:server_id, :metric_name, :value, :severity)
|
|
||||||
");
|
|
||||||
$stmt->execute([
|
|
||||||
':server_id' => $serverId,
|
|
||||||
':metric_name' => $metricName,
|
|
||||||
':value' => $value,
|
|
||||||
':severity' => $severity
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createServiceAlert($serverId, $serviceName, $status)
|
|
||||||
{
|
|
||||||
// Проверяем есть ли уже неразрешенный алерт для этого сервиса
|
|
||||||
$stmt = $this->pdo->prepare("
|
|
||||||
SELECT id FROM service_alerts
|
|
||||||
WHERE server_id = :server_id AND service_name = :service_name AND resolved = FALSE
|
|
||||||
ORDER BY created_at DESC LIMIT 1
|
|
||||||
");
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
':server_id' => $serverId,
|
|
||||||
':service_name' => $serviceName
|
|
||||||
]);
|
|
||||||
|
|
||||||
$existingAlert = $stmt->fetch();
|
|
||||||
|
|
||||||
// Если алерта нет или он уже разрешен - создаем новый
|
|
||||||
if (!$existingAlert) {
|
|
||||||
$stmt = $this->pdo->prepare("
|
|
||||||
INSERT INTO service_alerts (server_id, service_name, status, severity)
|
|
||||||
VALUES (:server_id, :service_name, :status, 'critical')
|
|
||||||
");
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
':server_id' => $serverId,
|
|
||||||
':service_name' => $serviceName,
|
|
||||||
':status' => $status
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ class DashboardController
|
||||||
// Получаем статистику
|
// Получаем статистику
|
||||||
$stats = $this->serverModel->getStats();
|
$stats = $this->serverModel->getStats();
|
||||||
|
|
||||||
// Получаем список серверов с последними метриками
|
// Получаем список серверов со статусами для цветных карточек
|
||||||
$servers = $this->serverModel->getAll();
|
$servers = $this->serverModel->getServersWithStatus();
|
||||||
|
|
||||||
|
|
||||||
$templateData = [
|
$templateData = [
|
||||||
'title' => 'Дашборд мониторинга',
|
'title' => 'Дашборд мониторинга',
|
||||||
|
|
@ -32,6 +33,7 @@ class DashboardController
|
||||||
'servers' => $servers
|
'servers' => $servers
|
||||||
];
|
];
|
||||||
|
|
||||||
|
file_put_contents("/tmp/dashboard_debug.log", json_encode($servers) . "\n", FILE_APPEND);
|
||||||
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Models\Model;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Slim\Views\Twig;
|
use Slim\Views\Twig;
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
class ServerDetailController extends Model
|
class ServerDetailController extends Model
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
// src/Middlewares/FlashMiddleware.php
|
||||||
|
|
||||||
|
namespace App\Middlewares;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class FlashMiddleware
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
// Делаем flash доступным в Twig
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Передаём flash в глобальные переменные Twig
|
||||||
|
// Это будет обработано в layout
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,9 +23,14 @@ class SessionMiddleware
|
||||||
$sessionData = [
|
$sessionData = [
|
||||||
'user_id' => $_SESSION['user_id'] ?? null,
|
'user_id' => $_SESSION['user_id'] ?? null,
|
||||||
'username' => $_SESSION['username'] ?? null,
|
'username' => $_SESSION['username'] ?? null,
|
||||||
'role' => $_SESSION['role'] ?? null
|
'role' => $_SESSION['role'] ?? null,
|
||||||
|
'flash_message' => $_SESSION['flash_message'] ?? null,
|
||||||
|
'flash_type' => $_SESSION['flash_type'] ?? null
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Очищаем flash после чтения
|
||||||
|
unset($_SESSION['flash_message'], $_SESSION['flash_type']);
|
||||||
|
|
||||||
// Получаем environment и добавляем session в глобальный контекст
|
// Получаем environment и добавляем session в глобальный контекст
|
||||||
$environment = $this->twig->getEnvironment();
|
$environment = $this->twig->getEnvironment();
|
||||||
$environment->addGlobal('session', $sessionData);
|
$environment->addGlobal('session', $sessionData);
|
||||||
|
|
|
||||||
|
|
@ -38,22 +38,92 @@ class Server
|
||||||
{
|
{
|
||||||
$stats = [];
|
$stats = [];
|
||||||
|
|
||||||
// Общее количество серверов
|
|
||||||
$stmt = $this->db->query("SELECT COUNT(*) as total FROM servers");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM servers");
|
||||||
$stats['total_servers'] = $stmt->fetch()['total'];
|
$stats['total_servers'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Количество групп
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM servers WHERE last_metrics_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)");
|
||||||
|
$stats['servers_with_metrics'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
$stmt = $this->db->query("SELECT COUNT(*) as total FROM server_groups");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM server_groups");
|
||||||
$stats['total_groups'] = $stmt->fetch()['total'];
|
$stats['total_groups'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Активные алерты (warning)
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE");
|
||||||
|
$stats['alerts_count'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'warning'");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'warning'");
|
||||||
$stats['warnings'] = $stmt->fetch()['total'];
|
$stats['warnings'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
// Активные алерты (critical)
|
|
||||||
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'critical'");
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'critical'");
|
||||||
$stats['criticals'] = $stmt->fetch()['total'];
|
$stats['criticals'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить серверы с вычисленным статусом
|
||||||
|
* Status вычисляется в SQL через CASE WHEN
|
||||||
|
*/
|
||||||
|
public function getServersWithStatus()
|
||||||
|
{
|
||||||
|
$stmt = $this->db->query("
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.name,
|
||||||
|
s.address,
|
||||||
|
s.description,
|
||||||
|
s.last_metrics_at,
|
||||||
|
s.created_at,
|
||||||
|
sg.name as group_name,
|
||||||
|
sg.icon as group_icon,
|
||||||
|
sg.color as group_color,
|
||||||
|
TIMESTAMPDIFF(SECOND, s.last_metrics_at, NOW()) as seconds_since_update,
|
||||||
|
CASE
|
||||||
|
WHEN s.last_metrics_at IS NULL THEN 'offline'
|
||||||
|
WHEN TIMESTAMPDIFF(SECOND, s.last_metrics_at, NOW()) > 300 THEN 'offline'
|
||||||
|
ELSE 'online'
|
||||||
|
END as status
|
||||||
|
FROM servers s
|
||||||
|
LEFT JOIN server_groups sg ON s.group_id = sg.id
|
||||||
|
ORDER BY s.name
|
||||||
|
");
|
||||||
|
$servers = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($servers as &$server) {
|
||||||
|
// Получаем последние метрики
|
||||||
|
$stmt2 = $this->db->prepare("
|
||||||
|
SELECT mn.name, sm.value, mn.unit
|
||||||
|
FROM server_metrics sm
|
||||||
|
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
||||||
|
WHERE sm.server_id = :server_id
|
||||||
|
AND mn.name NOT LIKE '%_proc'
|
||||||
|
ORDER BY sm.created_at DESC
|
||||||
|
LIMIT 10
|
||||||
|
");
|
||||||
|
$stmt2->execute([':server_id' => $server['id']]);
|
||||||
|
$metrics = $stmt2->fetchAll();
|
||||||
|
|
||||||
|
$server['latest_metrics'] = [];
|
||||||
|
foreach ($metrics as $m) {
|
||||||
|
if (!isset($server['latest_metrics'][$m['name']])) {
|
||||||
|
$server['latest_metrics'][$m['name']] = $m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем активные алерты
|
||||||
|
$stmt3 = $this->db->prepare("
|
||||||
|
SELECT COUNT(*) as cnt FROM alerts
|
||||||
|
WHERE server_id = :server_id AND resolved = FALSE
|
||||||
|
");
|
||||||
|
$stmt3->execute([':server_id' => $server['id']]);
|
||||||
|
$activeAlerts = $stmt3->fetch()['cnt'];
|
||||||
|
|
||||||
|
if ($server['status'] === 'online' && $activeAlerts > 0) {
|
||||||
|
$server['status'] = 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
$server['active_alerts'] = (int)$activeAlerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $servers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
<?php
|
||||||
|
// src/Services/NotificationService.php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
|
||||||
|
class NotificationService
|
||||||
|
{
|
||||||
|
private $pdo;
|
||||||
|
private $settings;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->pdo = DatabaseConfig::getInstance();
|
||||||
|
$this->loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadSettings()
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM global_notification_settings WHERE id = 1");
|
||||||
|
$stmt->execute();
|
||||||
|
$this->settings = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить уведомление о алерте
|
||||||
|
*/
|
||||||
|
public function sendAlertNotification($serverName, $metricName, $value, $severity, $threshold)
|
||||||
|
{
|
||||||
|
$severityText = $severity === 'critical' ? 'КРИТИЧЕСКИЙ' : 'ПРЕДУПРЕЖДЕНИЕ';
|
||||||
|
$subject = "🚨 {$severityText}: Превышение порога {$metricName}";
|
||||||
|
$message = "Сервер: {$serverName}\n";
|
||||||
|
$message .= "Метрика: {$metricName}\n";
|
||||||
|
$message .= "Значение: {$value}\n";
|
||||||
|
$message .= "Порог: {$threshold}\n";
|
||||||
|
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
||||||
|
$message .= "Серьёзность: {$severityText}";
|
||||||
|
|
||||||
|
// Отправка Email
|
||||||
|
if (!empty($this->settings['email_enabled']) && !empty($this->settings['smtp_host'])) {
|
||||||
|
$this->sendEmail($subject, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправка Telegram
|
||||||
|
if (!empty($this->settings['telegram_enabled']) && !empty($this->settings['telegram_bot_token'])) {
|
||||||
|
$this->sendTelegram($subject, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить тестовое уведомление
|
||||||
|
*/
|
||||||
|
public function sendTestNotification($email, $telegramChatId)
|
||||||
|
{
|
||||||
|
$subject = "✅ Тест уведомлений — Система мониторинга";
|
||||||
|
$message = "Это тестовое сообщение от системы мониторинга.\n";
|
||||||
|
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
||||||
|
$message .= "Если вы получили это сообщение — уведомления работают корректно.";
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
// Тест Email
|
||||||
|
if (!empty($email)) {
|
||||||
|
$results['email'] = $this->sendEmail($subject, $message, $email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тест Telegram
|
||||||
|
if (!empty($telegramChatId)) {
|
||||||
|
$results['telegram'] = $this->sendTelegram($subject, $message, $telegramChatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить Email
|
||||||
|
*/
|
||||||
|
private function sendEmail($subject, $message, $overrideEmail = null)
|
||||||
|
{
|
||||||
|
$to = $overrideEmail ?? $this->settings['smtp_from_email'];
|
||||||
|
if (empty($to) || empty($this->settings['smtp_host'])) {
|
||||||
|
return ['success' => false, 'error' => 'Не настроен SMTP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = "From: {$this->settings['smtp_from_email']}\r\n";
|
||||||
|
$headers .= "Reply-To: {$this->settings['smtp_from_email']}\r\n";
|
||||||
|
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||||
|
$headers .= "X-Mailer: PHP/" . phpversion();
|
||||||
|
|
||||||
|
// Если SMTP с авторизацией — используем PHPMailer через curl
|
||||||
|
if (!empty($this->settings['smtp_username'])) {
|
||||||
|
return $this->sendEmailViaSmtp($to, $subject, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простая отправка через mail()
|
||||||
|
if (mail($to, $subject, $message, $headers)) {
|
||||||
|
return ['success' => true, 'message' => 'Email отправлен'];
|
||||||
|
} else {
|
||||||
|
return ['success' => false, 'error' => 'Ошибка отправки Email'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить через SMTP с авторизацией (curl)
|
||||||
|
*/
|
||||||
|
private function sendEmailViaSmtp($to, $subject, $message)
|
||||||
|
{
|
||||||
|
$host = $this->settings['smtp_host'];
|
||||||
|
$port = $this->settings['smtp_port'];
|
||||||
|
$username = $this->settings['smtp_username'];
|
||||||
|
$password = $this->settings['smtp_password'];
|
||||||
|
$from = $this->settings['smtp_from_email'];
|
||||||
|
$encryption = $this->settings['smtp_encryption'];
|
||||||
|
|
||||||
|
// Формируем URL
|
||||||
|
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||||
|
$url = "{$scheme}://{$host}:{$port}";
|
||||||
|
|
||||||
|
// Формируем email
|
||||||
|
$email = "Date: " . date('r') . "\r\n";
|
||||||
|
$email .= "From: {$from}\r\n";
|
||||||
|
$email .= "To: {$to}\r\n";
|
||||||
|
$email .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n";
|
||||||
|
$email .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||||
|
$email .= "\r\n" . $message . "\r\n";
|
||||||
|
|
||||||
|
// Используем curl для отправки через SMTP
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_MAIL_FROM, $from);
|
||||||
|
curl_setopt($ch, CURLOPT_MAIL_RCPT, [$to]);
|
||||||
|
curl_setopt($ch, CURLOPT_UPLOAD, true);
|
||||||
|
curl_setopt($ch, CURLOPT_READDATA, fopen('php://temp', 'r+'));
|
||||||
|
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($email));
|
||||||
|
curl_setopt($ch, CURLOPT_USERNAME, $username);
|
||||||
|
curl_setopt($ch, CURLOPT_PASSWORD, $password);
|
||||||
|
curl_setopt($ch, CURLOPT_USE_SSL, ($encryption === 'none') ? CURLUSESSL_NONE : CURLUSESSL_ALL);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
// Для отправки через curl нужно записать данные в stream
|
||||||
|
$temp = fopen('php://temp', 'w+');
|
||||||
|
fwrite($temp, $email);
|
||||||
|
rewind($temp);
|
||||||
|
curl_setopt($ch, CURLOPT_INFILE, $temp);
|
||||||
|
curl_setopt($ch, CURLOPT_UPLOAD, true);
|
||||||
|
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
fclose($temp);
|
||||||
|
|
||||||
|
if ($result !== false && in_array($httpCode, [250, 221])) {
|
||||||
|
return ['success' => true, 'message' => 'Email отправлен через SMTP'];
|
||||||
|
} else {
|
||||||
|
return ['success' => false, 'error' => "SMTP ошибка: " . ($error ?: "HTTP {$httpCode}")];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить Telegram сообщение
|
||||||
|
*/
|
||||||
|
private function sendTelegram($subject, $message, $overrideChatId = null)
|
||||||
|
{
|
||||||
|
$botToken = $this->settings['telegram_bot_token'];
|
||||||
|
$chatId = $overrideChatId ?? $this->settings['telegram_chat_id'];
|
||||||
|
|
||||||
|
if (empty($botToken) || empty($chatId)) {
|
||||||
|
return ['success' => false, 'error' => 'Не настроен Telegram'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = "<b>{$subject}</b>\n\n" . str_replace("\n", "\n", $message);
|
||||||
|
|
||||||
|
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
|
||||||
|
|
||||||
|
$postData = [
|
||||||
|
'chat_id' => $chatId,
|
||||||
|
'text' => $text,
|
||||||
|
'parse_mode' => 'HTML'
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
|
|
||||||
|
// Настройки прокси для Telegram
|
||||||
|
$proxy = $this->settings['telegram_proxy'] ?? 'http://127.0.0.1:1081';
|
||||||
|
if (!empty($proxy)) {
|
||||||
|
curl_setopt($ch, CURLOPT_PROXY, $proxy);
|
||||||
|
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
return ['success' => false, 'error' => "Ошибка соединения: {$error}"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if ($httpCode === 200 && isset($data['ok']) && $data['ok']) {
|
||||||
|
return ['success' => true, 'message' => 'Telegram сообщение отправлено'];
|
||||||
|
} else {
|
||||||
|
$errorMsg = $data['description'] ?? 'Неизвестная ошибка';
|
||||||
|
return ['success' => false, 'error' => "Telegram API: {$errorMsg}"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить настройки
|
||||||
|
*/
|
||||||
|
public function getSettings()
|
||||||
|
{
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить настройки
|
||||||
|
*/
|
||||||
|
public function saveSettings($data)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE global_notification_settings SET
|
||||||
|
smtp_host = :smtp_host,
|
||||||
|
smtp_port = :smtp_port,
|
||||||
|
smtp_username = :smtp_username,
|
||||||
|
smtp_password = :smtp_password,
|
||||||
|
smtp_encryption = :smtp_encryption,
|
||||||
|
smtp_from_email = :smtp_from_email,
|
||||||
|
telegram_bot_token = :telegram_bot_token,
|
||||||
|
telegram_chat_id = :telegram_chat_id,
|
||||||
|
telegram_proxy = :telegram_proxy,
|
||||||
|
email_enabled = :email_enabled,
|
||||||
|
telegram_enabled = :telegram_enabled,
|
||||||
|
notify_on_warning = :notify_on_warning,
|
||||||
|
notify_on_critical = :notify_on_critical
|
||||||
|
WHERE id = 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':smtp_host' => $data['smtp_host'] ?? '',
|
||||||
|
':smtp_port' => (int)($data['smtp_port'] ?? 587),
|
||||||
|
':smtp_username' => $data['smtp_username'] ?? '',
|
||||||
|
':smtp_password' => $data['smtp_password'] ?? '',
|
||||||
|
':smtp_encryption' => $data['smtp_encryption'] ?? 'tls',
|
||||||
|
':smtp_from_email' => $data['smtp_from_email'] ?? '',
|
||||||
|
':telegram_bot_token' => $data['telegram_bot_token'] ?? '',
|
||||||
|
':telegram_chat_id' => $data['telegram_chat_id'] ?? '',
|
||||||
|
':telegram_proxy' => $data['telegram_proxy'] ?? 'http://127.0.0.1:1081',
|
||||||
|
':email_enabled' => !empty($data['email_enabled']) ? 1 : 0,
|
||||||
|
':telegram_enabled' => !empty($data['telegram_enabled']) ? 1 : 0,
|
||||||
|
':notify_on_warning' => !empty($data['notify_on_warning']) ? 1 : 0,
|
||||||
|
':notify_on_critical' => !empty($data['notify_on_critical']) ? 1 : 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Перезагружаем настройки
|
||||||
|
$this->loadSettings();
|
||||||
|
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,79 +1,197 @@
|
||||||
{% extends "layout.twig" %}
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-12">
|
||||||
<div class="card">
|
<h2><i class="fas fa-bell"></i> Настройки уведомлений</h2>
|
||||||
<div class="card-header">
|
<p class="text-muted">Настройте отправку уведомлений через Email и Telegram</p>
|
||||||
<h3><i class="fas fa-bell"></i> Настройки уведомлений</h3>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if session.flash_message is defined and session.flash_message %}
|
||||||
|
<div class="alert alert-{{ session.flash_type == 'error' ? 'danger' : 'success' }} alert-dismissible fade show">
|
||||||
|
{{ session.flash_message|nl2br }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/admin/notifications/save">
|
||||||
|
<input type="hidden" name="csrf_name" value="{{ csrf_name }}">
|
||||||
|
<input type="hidden" name="csrf_value" value="{{ csrf_value }}">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Email настройки -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-envelope"></i> Email (SMTP)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="email_enabled" id="email_enabled"
|
||||||
|
{% if settings.email_enabled %}checked{% endif %}
|
||||||
|
onchange="toggleSection('email_settings', this.checked)">
|
||||||
|
<label class="form-check-label" for="email_enabled">
|
||||||
|
Включить Email уведомления
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="email_settings" style="{% if not settings.email_enabled %}display:none{% endif %}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_host" class="form-label">SMTP сервер</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_host" name="smtp_host"
|
||||||
|
value="{{ settings.smtp_host }}" placeholder="smtp.gmail.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_port" class="form-label">Порт SMTP</label>
|
||||||
|
<input type="number" class="form-control" id="smtp_port" name="smtp_port"
|
||||||
|
value="{{ settings.smtp_port }}" placeholder="587">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_username" class="form-label">Логин SMTP</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_username" name="smtp_username"
|
||||||
|
value="{{ settings.smtp_username }}" placeholder="your@email.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_password" class="form-label">Пароль SMTP</label>
|
||||||
|
<input type="password" class="form-control" id="smtp_password" name="smtp_password"
|
||||||
|
value="{{ settings.smtp_password }}" placeholder="Пароль приложения">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_encryption" class="form-label">Шифрование</label>
|
||||||
|
<select class="form-select" id="smtp_encryption" name="smtp_encryption">
|
||||||
|
<option value="tls" {% if settings.smtp_encryption == 'tls' %}selected{% endif %}>TLS</option>
|
||||||
|
<option value="ssl" {% if settings.smtp_encryption == 'ssl' %}selected{% endif %}>SSL</option>
|
||||||
|
<option value="none" {% if settings.smtp_encryption == 'none' %}selected{% endif %}>Без шифрования</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="smtp_from_email" class="form-label">Email отправителя</label>
|
||||||
|
<input type="email" class="form-control" id="smtp_from_email" name="smtp_from_email"
|
||||||
|
value="{{ settings.smtp_from_email }}" placeholder="monitor@mirv.top">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram настройки -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="fab fa-telegram-plane"></i> Telegram Bot</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="telegram_enabled" id="telegram_enabled"
|
||||||
|
{% if settings.telegram_enabled %}checked{% endif %}
|
||||||
|
onchange="toggleSection('telegram_settings', this.checked)">
|
||||||
|
<label class="form-check-label" for="telegram_enabled">
|
||||||
|
Включить Telegram уведомления
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="telegram_settings" style="{% if not settings.telegram_enabled %}display:none{% endif %}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="telegram_bot_token" class="form-label">Токен бота</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_bot_token" name="telegram_bot_token"
|
||||||
|
value="{{ settings.telegram_bot_token }}" placeholder="123456:ABC-DEF...">
|
||||||
|
<div class="form-text">Получите у @BotFather в Telegram</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="telegram_chat_id" class="form-label">Chat ID</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id"
|
||||||
|
value="{{ settings.telegram_chat_id }}" placeholder="-1001234567890">
|
||||||
|
<div class="form-text">ID чата или группы (отрицательное число для групп)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="telegram_proxy" class="form-label">Прокси для Telegram</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_proxy" name="telegram_proxy"
|
||||||
|
value="{{ settings.telegram_proxy|default('http://127.0.0.1:1081') }}"
|
||||||
|
placeholder="http://127.0.0.1:1081">
|
||||||
|
<div class="form-text">Оставьте пустым, если прокси не нужен</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info small">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<strong>Как настроить:</strong>
|
||||||
|
<ol class="mb-0 ps-3">
|
||||||
|
<li>Создайте бота через @BotFather</li>
|
||||||
|
<li>Добавьте бота в чат/группу</li>
|
||||||
|
<li>Отправьте сообщение в чат</li>
|
||||||
|
<li>Узнайте Chat ID через https://api.telegram.org/bot[TOKEN]/getUpdates</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Типы уведомлений -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-sliders-h"></i> Когда отправлять уведомления</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="/admin/notifications">
|
|
||||||
<h5><i class="fas fa-envelope"></i> Email уведомления</h5>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="smtp_host" class="form-label">SMTP сервер</label>
|
<div class="form-check form-switch">
|
||||||
<input type="text" class="form-control" id="smtp_host" name="smtp_host" placeholder="smtp.gmail.com">
|
<input class="form-check-input" type="checkbox" name="notify_on_warning" id="notify_on_warning"
|
||||||
</div>
|
{% if settings.notify_on_warning %}checked{% endif %}>
|
||||||
<div class="col-md-6">
|
<label class="form-check-label" for="notify_on_warning">
|
||||||
<label for="smtp_port" class="form-label">Порт</label>
|
<i class="fas fa-exclamation-triangle text-warning"></i>
|
||||||
<input type="number" class="form-control" id="smtp_port" name="smtp_port" placeholder="587">
|
При предупреждениях (warning)
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-2">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="smtp_username" class="form-label">Имя пользователя</label>
|
|
||||||
<input type="text" class="form-control" id="smtp_username" name="smtp_username" placeholder="user@example.com">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="smtp_password" class="form-label">Пароль</label>
|
|
||||||
<input type="password" class="form-control" id="smtp_password" name="smtp_password" placeholder="••••••••">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mt-2">
|
|
||||||
<input class="form-check-input" type="checkbox" id="smtp_secure" name="smtp_secure">
|
|
||||||
<label class="form-check-label" for="smtp_secure">
|
|
||||||
Использовать безопасное соединение (SSL/TLS)
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h5><i class="fab fa-telegram"></i> Telegram уведомления</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="telegram_bot_token" class="form-label">Bot Token</label>
|
<div class="form-check form-switch">
|
||||||
<input type="text" class="form-control" id="telegram_bot_token" name="telegram_bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ">
|
<input class="form-check-input" type="checkbox" name="notify_on_critical" id="notify_on_critical"
|
||||||
|
{% if settings.notify_on_critical %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="notify_on_critical">
|
||||||
|
<i class="fas fa-radiation text-danger"></i>
|
||||||
|
При критических ошибках (critical)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="telegram_chat_id" class="form-label">Chat ID</label>
|
|
||||||
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id" placeholder="-1001234567890">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<!-- Кнопки -->
|
||||||
<h5><i class="fas fa-mobile-alt"></i> SMS уведомления</h5>
|
<div class="row mb-4">
|
||||||
<div class="row">
|
<div class="col-12 d-flex justify-content-between">
|
||||||
<div class="col-md-6">
|
<div>
|
||||||
<label for="sms_api_key" class="form-label">API ключ</label>
|
|
||||||
<input type="text" class="form-control" id="sms_api_key" name="sms_api_key" placeholder="API ключ сервиса">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="sms_sender" class="form-label">Отправитель</label>
|
|
||||||
<input type="text" class="form-control" id="sms_sender" name="sms_sender" placeholder="MyCompany">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="fas fa-save"></i> Сохранить настройки
|
<i class="fas fa-save"></i> Сохранить настройки
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/notifications/test" class="btn btn-outline-success"
|
||||||
|
onclick="return confirm('Отправить тестовое уведомление?')">
|
||||||
|
<i class="fas fa-paper-plane"></i> Тест уведомлений
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
<script>
|
||||||
</div>
|
function toggleSection(id, show) {
|
||||||
</div>
|
document.getElementById(id).style.display = show ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -5,11 +5,18 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2><i class="fas fa-users-cog"></i> Управление пользователями</h2>
|
<h2><i class="fas fa-users-cog"></i> Управление пользователями</h2>
|
||||||
<a href="#" class="btn btn-primary">
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openCreateModal()">
|
||||||
<i class="fas fa-plus"></i> Добавить пользователя
|
<i class="fas fa-plus"></i> Добавить пользователя
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if session.flash_message is defined and session.flash_message %}
|
||||||
|
<div class="alert alert-{{ session.flash_type == 'error' ? 'danger' : 'success' }} alert-dismissible fade show">
|
||||||
|
{{ session.flash_message|nl2br }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if users|length > 0 %}
|
{% if users|length > 0 %}
|
||||||
|
|
@ -21,6 +28,8 @@
|
||||||
<th>Имя пользователя</th>
|
<th>Имя пользователя</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Роль</th>
|
<th>Роль</th>
|
||||||
|
<th>Telegram Chat ID</th>
|
||||||
|
<th>Email для алертов</th>
|
||||||
<th>Дата создания</th>
|
<th>Дата создания</th>
|
||||||
<th>Действия</th>
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -29,7 +38,7 @@
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.id }}</td>
|
<td>{{ user.id }}</td>
|
||||||
<td>{{ user.username }}</td>
|
<td><strong>{{ user.username }}</strong></td>
|
||||||
<td>{{ user.email|default('-') }}</td>
|
<td>{{ user.email|default('-') }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.role == 'admin' %}
|
{% if user.role == 'admin' %}
|
||||||
|
|
@ -38,16 +47,20 @@
|
||||||
<span class="badge bg-secondary"><i class="fas fa-user"></i> Пользователь</span>
|
<span class="badge bg-secondary"><i class="fas fa-user"></i> Пользователь</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.created_at|date('d.m.Y H:i:s') }}</td>
|
<td>{{ user.telegram_chat_id|default('-') }}</td>
|
||||||
|
<td>{{ user.email_for_alerts|default('-') }}</td>
|
||||||
|
<td>{{ user.created_at|date('d.m.Y H:i') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/admin/users/{{ user.id }}/edit" class="btn btn-sm btn-outline-primary">
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
<i class="fas fa-edit"></i> Редактировать
|
data-bs-toggle="modal" data-bs-target="#userModal"
|
||||||
</a>
|
onclick="openEditModal({{ user.id }}, '{{ user.username }}', '{{ user.email }}', '{{ user.role }}', '{{ user.telegram_chat_id }}', '{{ user.email_for_alerts }}')">
|
||||||
<form action="/admin/users/{{ user.id }}" method="post" style="display: inline-block;" onsubmit="return confirm('Вы уверены, что хотите удалить этого пользователя?');">
|
<i class="fas fa-edit"></i>
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
|
||||||
<i class="fas fa-trash"></i> Удалить
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<a href="/admin/users/{{ user.id }}/delete"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
onclick="return confirm('Удалить пользователя {{ user.username }}?')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -55,16 +68,107 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
||||||
<p class="lead">Пользователи пока не созданы</p>
|
<p class="lead">Пользователи не найдены</p>
|
||||||
<a href="#" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus"></i> Создать первого пользователя
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальная форма -->
|
||||||
|
<div class="modal fade" id="userModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="userForm" method="post" action="/admin/users/save">
|
||||||
|
<input type="hidden" name="csrf_name" value="{{ csrf_name }}">
|
||||||
|
<input type="hidden" name="csrf_value" value="{{ csrf_value }}">
|
||||||
|
<input type="hidden" id="user_id" name="user_id" value="">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalTitle">Добавить пользователя</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Имя пользователя *</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Пароль <span id="password_hint" class="text-muted small">(оставьте пустым чтобы не менять)</span></label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="role" class="form-label">Роль</label>
|
||||||
|
<select class="form-select" id="role" name="role">
|
||||||
|
<option value="user">Пользователь</option>
|
||||||
|
<option value="admin">Администратор</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h6><i class="fas fa-bell"></i> Настройки уведомлений</h6>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="telegram_chat_id" class="form-label">Telegram Chat ID</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id"
|
||||||
|
placeholder="1150922 или -1001234567890">
|
||||||
|
<div class="form-text small">ID личного чата или группы для уведомлений</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email_for_alerts" class="form-label">Email для алертов</label>
|
||||||
|
<input type="email" class="form-control" id="email_for_alerts" name="email_for_alerts"
|
||||||
|
placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openCreateModal() {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Добавить пользователя';
|
||||||
|
document.getElementById('user_id').value = '';
|
||||||
|
document.getElementById('username').value = '';
|
||||||
|
document.getElementById('email').value = '';
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
document.getElementById('password').required = true;
|
||||||
|
document.getElementById('password_hint').style.display = 'none';
|
||||||
|
document.getElementById('role').value = 'user';
|
||||||
|
document.getElementById('telegram_chat_id').value = '';
|
||||||
|
document.getElementById('email_for_alerts').value = '';
|
||||||
|
document.getElementById('userForm').action = '/admin/users/save';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(id, username, email, role, telegramChatId, emailForAlerts) {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Редактировать пользователя';
|
||||||
|
document.getElementById('user_id').value = id;
|
||||||
|
document.getElementById('username').value = username;
|
||||||
|
document.getElementById('email').value = email === '' ? '' : email;
|
||||||
|
document.getElementById('password').required = false;
|
||||||
|
document.getElementById('password_hint').style.display = 'inline';
|
||||||
|
document.getElementById('role').value = role;
|
||||||
|
document.getElementById('telegram_chat_id').value = telegramChatId === '' ? '' : telegramChatId;
|
||||||
|
document.getElementById('email_for_alerts').value = emailForAlerts === '' ? '' : emailForAlerts;
|
||||||
|
document.getElementById('userForm').action = '/admin/users/save';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,125 +1,234 @@
|
||||||
{% extends "layout.twig" %}
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h2><i class="fas fa-tachometer-alt"></i> Дашборд мониторинга</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statistics Cards -->
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||||
|
<h2><i class="fas fa-tachometer-alt"></i> Дашборд мониторинга</h2>
|
||||||
|
<div>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Добавить сервер
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статистика -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3 col-sm-6 mb-3">
|
||||||
|
<div class="card text-white bg-primary">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="fas fa-server text-info fa-2x mb-2"></i>
|
<i class="fas fa-server fa-2x mb-2"></i>
|
||||||
<h3>{{ stats.total_servers }}</h3>
|
<h3>{{ stats.total_servers }}</h3>
|
||||||
<p class="text-muted mb-0">Всего серверов</p>
|
<p class="mb-0">Всего серверов</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-3 col-sm-6 mb-3">
|
||||||
<div class="card">
|
<div class="card text-white bg-success">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="fas fa-chart-line text-success fa-2x mb-2"></i>
|
<i class="fas fa-check-circle fa-2x mb-2"></i>
|
||||||
<h3>{{ stats.servers_with_metrics }}</h3>
|
<h3>{{ stats.servers_with_metrics }}</h3>
|
||||||
<p class="text-muted mb-0">С метриками</p>
|
<p class="mb-0">С метриками</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-3 col-sm-6 mb-3">
|
||||||
<div class="card">
|
<div class="card text-white bg-warning">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2"></i>
|
<i class="fas fa-exclamation-triangle fa-2x mb-2"></i>
|
||||||
<h3>{{ stats.alerts_count }}</h3>
|
<h3>{{ stats.warnings }}</h3>
|
||||||
<p class="text-muted mb-0">Активных алертов</p>
|
<p class="mb-0">Предупреждения</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-sm-6 mb-3">
|
||||||
|
<div class="card text-white bg-danger">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-radiation fa-2x mb-2"></i>
|
||||||
|
<h3>{{ stats.criticals }}</h3>
|
||||||
|
<p class="mb-0">Критические</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Servers List -->
|
<!-- Карточки серверов -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
{% for server in servers %}
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="col-xl-4 col-lg-6 col-md-6 mb-4">
|
||||||
<h4 class="mb-0"><i class="fas fa-server"></i> Серверы</h4>
|
<div class="card h-100 server-card" data-server-id="{{ server.id }}" data-status="{{ server.status }}">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center
|
||||||
|
{% if server.status == 'online' %}bg-success text-white
|
||||||
|
{% elseif server.status == 'warning' %}bg-warning text-dark
|
||||||
|
{% else %}bg-danger text-white{% endif %}">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-server"></i> {{ server.name }}
|
||||||
|
</h5>
|
||||||
<div>
|
<div>
|
||||||
<a href="/servers/create" class="btn btn-sm btn-outline-primary me-2">
|
{% if server.status == 'online' %}
|
||||||
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить сервер</span>
|
<span class="badge bg-light text-dark">
|
||||||
</a>
|
<i class="fas fa-check-circle"></i> Онлайн
|
||||||
<a href="/servers" class="btn btn-sm btn-outline-secondary">
|
</span>
|
||||||
<i class="fas fa-list"></i> <span class="d-none d-sm-inline">Все серверы</span>
|
{% elseif server.status == 'warning' %}
|
||||||
</a>
|
<span class="badge bg-dark">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Внимание
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
<i class="fas fa-times-circle"></i> Оффлайн
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if servers|length > 0 %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Название</th>
|
|
||||||
<th>Адрес</th>
|
|
||||||
<th>Группа</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Последние метрики</th>
|
|
||||||
<th>Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for server in servers %}
|
|
||||||
<tr>
|
|
||||||
<td><strong>{{ server.name }}</strong></td>
|
|
||||||
<td>{{ server.address|default('-') }}</td>
|
|
||||||
<td>
|
|
||||||
{% if server.group_name %}
|
{% if server.group_name %}
|
||||||
|
<div class="mb-2">
|
||||||
<span class="badge" style="background-color: {{ server.group_color|default('#6c757d') }}">
|
<span class="badge" style="background-color: {{ server.group_color|default('#6c757d') }}">
|
||||||
<i class="fas {{ server.group_icon|default('fa-box') }} me-1"></i>{{ server.group_name }}
|
<i class="fas {{ server.group_icon|default('fa-box') }}"></i> {{ server.group_name }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">Без группы</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if server.last_metrics_at %}
|
|
||||||
<span class="badge bg-success">Активен</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning">Нет метрик</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if server.last_metrics_at %}
|
|
||||||
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/servers/{{ server.id }}" class="btn btn-sm btn-outline-info" title="Просмотр">
|
|
||||||
<i class="fas fa-eye"></i> <span class="d-none d-sm-inline">Просмотр</span>
|
|
||||||
</a>
|
|
||||||
<a href="/servers/{{ server.id }}/edit" class="btn btn-sm btn-outline-primary" title="Редактировать">
|
|
||||||
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<i class="fas fa-server fa-3x text-muted mb-3"></i>
|
|
||||||
<p class="lead">Серверы пока не добавлены</p>
|
|
||||||
<a href="/servers/create" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить первый сервер</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if server.description %}
|
||||||
|
<p class="text-muted small mb-3">{{ server.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-2"><span class="badge bg-info">Статус: {{ server.status }}</span></div>
|
||||||
|
|
||||||
|
<!-- Метрики -->
|
||||||
|
<div class="row">
|
||||||
|
{% if server.latest_metrics['cpu_load'] is defined %}
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small class="text-muted"><i class="fas fa-microchip"></i> CPU</small>
|
||||||
|
<strong>{{ server.latest_metrics['cpu_load'].value }}{{ server.latest_metrics['cpu_load'].unit }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar {% if server.latest_metrics['cpu_load'].value > 80 %}bg-danger{% elseif server.latest_metrics['cpu_load'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ server.latest_metrics['cpu_load'].value }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if server.latest_metrics['ram_used'] is defined %}
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small class="text-muted"><i class="fas fa-memory"></i> RAM</small>
|
||||||
|
<strong>{{ server.latest_metrics['ram_used'].value }}{{ server.latest_metrics['ram_used'].unit }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar {% if server.latest_metrics['ram_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['ram_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ server.latest_metrics['ram_used'].value }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if server.latest_metrics['disk_used'] is defined %}
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small class="text-muted"><i class="fas fa-hdd"></i> Диск</small>
|
||||||
|
<strong>{{ server.latest_metrics['disk_used'].value }}{{ server.latest_metrics['disk_used'].unit }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar {% if server.latest_metrics['disk_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['disk_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ server.latest_metrics['disk_used'].value }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if server.active_alerts > 0 %}
|
||||||
|
<div class="alert alert-danger py-2 mb-2">
|
||||||
|
<small>
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
Активных алертов: <strong>{{ server.active_alerts }}</strong>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Время обновления -->
|
||||||
|
<div class="text-muted small mt-2">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
Обновлено: {{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||||||
|
{% else %}
|
||||||
|
Метрики не получены
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-light">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="/servers/{{ server.id }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-chart-line"></i> Подробнее
|
||||||
|
</a>
|
||||||
|
<a href="/servers/{{ server.id }}/edit" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-edit"></i> Изменить
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-server fa-4x text-muted mb-3"></i>
|
||||||
|
<h4>Серверы пока не добавлены</h4>
|
||||||
|
<p class="lead text-muted">Добавьте первый сервер, чтобы начать мониторинг</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-plus"></i> Добавить первый сервер
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Автообновление каждые 30 секунд -->
|
||||||
|
<script>
|
||||||
|
// Автообновление через 30 секунд
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Визуальное обновление карточек с анимацией
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Добавляем плавную анимацию при наведении
|
||||||
|
document.querySelectorAll('.server-card').forEach(function(card) {
|
||||||
|
card.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
|
||||||
|
card.addEventListener('mouseenter', function() {
|
||||||
|
this.style.transform = 'translateY(-4px)';
|
||||||
|
this.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
|
||||||
|
});
|
||||||
|
card.addEventListener('mouseleave', function() {
|
||||||
|
this.style.transform = 'translateY(0)';
|
||||||
|
this.style.boxShadow = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.server-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.server-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.server-card .card-header {
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<main class="container mt-4">
|
<main class="container mt-4">
|
||||||
|
{% if session.flash_message is defined and session.flash_message %}
|
||||||
|
<div class="alert alert-{{ session.flash_type == "error" ? "danger" : "success" }} alert-dismissible fade show mb-3" role="alert">
|
||||||
|
{{ session.flash_message|nl2br }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,50 +90,44 @@
|
||||||
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<form method="get" class="row g-2 align-items-end" id="periodForm">
|
<!-- Отладка: period = {{ request.query.period }} -->
|
||||||
<input type="hidden" name="tab" value="metrics">
|
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
|
||||||
|
<div class="btn-group d-flex" role="group">
|
||||||
<!-- Пресеты -->
|
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
|
||||||
<div class="col-md-3">
|
24 часа
|
||||||
<label class="form-label mb-1">
|
</a>
|
||||||
<i class="fas fa-clock"></i> Быстрый выбор
|
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||||||
</label>
|
7 дней
|
||||||
<select class="form-select" id="presetSelect" onchange="applyPreset()">
|
</a>
|
||||||
<option value="">-- Выбрать --</option>
|
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||||||
<option value="30">Последние 30 минут</option>
|
30 дней
|
||||||
<option value="60">Последние 1 час</option>
|
</a>
|
||||||
<option value="120">Последние 2 часа</option>
|
|
||||||
<option value="360">Последние 6 часов</option>
|
|
||||||
<option value="720">Последние 12 часов</option>
|
|
||||||
<option value="1440">Последние 24 часа</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Дата начала -->
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label mb-1">
|
|
||||||
<i class="fas fa-calendar"></i> С
|
|
||||||
</label>
|
|
||||||
<input type="datetime-local" class="form-control" name="start" id="startDate"
|
|
||||||
value="{{ startDate }}" required>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Дата окончания -->
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label mb-1">
|
|
||||||
<i class="fas fa-calendar"></i> По
|
|
||||||
</label>
|
|
||||||
<input type="datetime-local" class="form-control" name="end" id="endDate"
|
|
||||||
value="{{ endDate }}" required>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mt-2">
|
||||||
<!-- Кнопки -->
|
<div class="col-md-12">
|
||||||
<div class="col-md-3">
|
<small class="text-muted">Масштаб:</small>
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<div class="btn-group ms-2" role="group">
|
||||||
<i class="fas fa-search"></i> Применить
|
<a href="?tab=metrics&period={{ period }}&zoom=auto" class="btn btn-sm {% if not zoom or zoom == 'auto' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
</button>
|
авто
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=1h" class="btn btn-sm {% if zoom == '1h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
1ч
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
6ч
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
24ч
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
7д
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
30д
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -416,6 +410,7 @@
|
||||||
|
|
||||||
<!-- Chart.js -->
|
<!-- Chart.js -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="/chartjs-plugin-crosshair.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// Функция для получения топ-процессов для указанного времени
|
// Функция для получения топ-процессов для указанного времени
|
||||||
|
|
@ -504,9 +499,9 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
|
||||||
var labels{{ metricName }} = [];
|
var labels{{ metricName }} = [];
|
||||||
var data{{ metricName }} = [];
|
var data{{ metricName }} = [];
|
||||||
|
|
||||||
{% for metric in metricData|slice(0, 400)|reverse %}
|
{% for metric in metricData|slice(0, 50000)|reverse %}
|
||||||
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
||||||
{% set time_format = metric.time_bucket and aggregation.aggregate_minutes >= 60 ? 'd.m H:i' : 'H:i' %}
|
{% set time_format = metric.time_bucket ? 'd.m H:i' : 'H:i' %}
|
||||||
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
||||||
data{{ metricName }}.push({{ metric.value|raw }});
|
data{{ metricName }}.push({{ metric.value|raw }});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -524,6 +519,19 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
crosshair: {
|
||||||
|
line: {
|
||||||
|
color: "rgba(100, 150, 255, 0.4)",
|
||||||
|
width: 1,
|
||||||
|
dashPattern: []
|
||||||
|
},
|
||||||
|
sync: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
|
|
@ -557,11 +565,21 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Прячем если курсор ушел с графика
|
// Прячем если курсор ушел с графика
|
||||||
if (!context.tooltip._active || context.tooltip._active.length === 0 || canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden) {
|
if (!context.tooltip._active || context.tooltip._active.length === 0) {
|
||||||
tooltipEl.style.opacity = 0;
|
tooltipEl.style.opacity = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка: курсор внутри chartArea (все 4 границы)
|
||||||
|
var chartArea = context.chart.chartArea;
|
||||||
|
if (chartArea && context.tooltip.caretX !== undefined && context.tooltip.caretY !== undefined) {
|
||||||
|
if (context.tooltip.caretX < chartArea.left || context.tooltip.caretX > chartArea.right ||
|
||||||
|
context.tooltip.caretY < chartArea.top || context.tooltip.caretY > chartArea.bottom) {
|
||||||
|
tooltipEl.style.opacity = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var dataIndex = context.tooltip._active[0].index;
|
var dataIndex = context.tooltip._active[0].index;
|
||||||
var time = labels{{ metricName }}[dataIndex];
|
var time = labels{{ metricName }}[dataIndex];
|
||||||
|
|
||||||
|
|
@ -595,7 +613,6 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
// Show tooltip
|
// Show tooltip
|
||||||
var position = context.chart.canvas.getBoundingClientRect();
|
var position = context.chart.canvas.getBoundingClientRect();
|
||||||
tooltipEl.innerHTML = lines.join('<br>');
|
tooltipEl.innerHTML = lines.join('<br>');
|
||||||
tooltipEl.style.visibility = 'visible';
|
|
||||||
tooltipEl.style.opacity = 1;
|
tooltipEl.style.opacity = 1;
|
||||||
tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX + 10 + 'px';
|
tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX + 10 + 'px';
|
||||||
tooltipEl.style.top = position.top + window.pageYOffset + context.tooltip.caretY + 'px';
|
tooltipEl.style.top = position.top + window.pageYOffset + context.tooltip.caretY + 'px';
|
||||||
|
|
@ -612,65 +629,40 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
});
|
});
|
||||||
|
|
||||||
// Скрывать tooltip при уходе курсора с canvas в любую сторону
|
// Скрывать tooltip при уходе курсора с canvas в любую сторону
|
||||||
var canvas{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}');
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas.addEventListener('mouseleave', function() {
|
||||||
if (canvas{{ metricName|replace({'-': '_', '.': '_'}) }}) {
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = false;
|
|
||||||
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mouseout', function() {
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = true;
|
|
||||||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||||||
if (tooltipEl) {
|
if (tooltipEl) {
|
||||||
tooltipEl.style.opacity = '0';
|
tooltipEl.style.opacity = 0;
|
||||||
}
|
}
|
||||||
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.tooltip._active = [];
|
||||||
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mouseleave', function() {
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = true;
|
|
||||||
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
|
||||||
if (tooltipEl) {
|
|
||||||
tooltipEl.style.opacity = '0';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mousemove', function() {
|
|
||||||
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// Применение пресета
|
|
||||||
function applyPreset() {
|
|
||||||
var select = document.getElementById("presetSelect");
|
|
||||||
var minutes = parseInt(select.value);
|
|
||||||
if (!minutes) return;
|
|
||||||
|
|
||||||
var now = new Date();
|
// Глобальный обработчик mousemove для скрытия тултипов при уходе курсора за пределы canvas
|
||||||
var startDate = new Date(now.getTime() - (minutes * 60 * 1000));
|
// (нужен т.к. crosshair плагин создаёт overlay canvas и перехватывает mouseleave)
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
// Форматируем для datetime-local (YYYY-MM-DDTHH:MM)
|
{% for metricName, metricData in metrics %}
|
||||||
function formatDateTimeLocal(date) {
|
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||||||
var year = date.getFullYear();
|
(function() {
|
||||||
var month = String(date.getMonth() + 1).padStart(2, "0");
|
var canvas = chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas;
|
||||||
var day = String(date.getDate()).padStart(2, "0");
|
var rect = canvas.getBoundingClientRect();
|
||||||
var hours = String(date.getHours()).padStart(2, "0");
|
var isOverCanvas = (e.clientX >= rect.left && e.clientX <= rect.right &&
|
||||||
var mins = String(date.getMinutes()).padStart(2, "0");
|
e.clientY >= rect.top && e.clientY <= rect.bottom);
|
||||||
return year + "-" + month + "-" + day + "T" + hours + ":" + mins;
|
if (!isOverCanvas) {
|
||||||
|
var tooltipEl = document.getElementById('chartjs-tooltip-{{ server.id }}-{{ metricName }}');
|
||||||
|
if (tooltipEl && tooltipEl.style.opacity !== '0') {
|
||||||
|
tooltipEl.style.opacity = 0;
|
||||||
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.tooltip._active = [];
|
||||||
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("startDate").value = formatDateTimeLocal(startDate);
|
|
||||||
document.getElementById("endDate").value = formatDateTimeLocal(now);
|
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
});
|
||||||
|
|
||||||
// Автоотправка формы при изменении дат (опционально)
|
|
||||||
// document.getElementById("startDate").addEventListener("change", function() {
|
|
||||||
// document.getElementById("periodForm").submit();
|
|
||||||
// });
|
|
||||||
// document.getElementById("endDate").addEventListener("change", function() {
|
|
||||||
// document.getElementById("periodForm").submit();
|
|
||||||
// });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue