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:
mirivlad 2026-04-13 00:59:19 +08:00
parent 9b64cee32c
commit b875e57e4c
34 changed files with 1494 additions and 995 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
vendor/
node_modules/
*.log

0
AGENTS.md Normal file → Executable file
View File

0
ARCHITECTURE.md Normal file → Executable file
View File

View File

@ -14,6 +14,7 @@
"autoload": {
"psr-4": {
"App\\": "src/",
"App\\Services\\": "src/Services",
"Config\\": "config/"
}
},

674
monitoring_system_dump.sql Normal file → Executable file
View File

@ -37,18 +37,6 @@ CREATE TABLE `agent_configs` (
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!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`
--
@ -69,18 +57,6 @@ CREATE TABLE `agent_tokens` (
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!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`
--
@ -104,13 +80,31 @@ CREATE TABLE `alerts` (
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `alerts`
-- Table structure for table `global_notification_settings`
--
LOCK TABLES `alerts` WRITE;
/*!40000 ALTER TABLE `alerts` DISABLE KEYS */;
/*!40000 ALTER TABLE `alerts` ENABLE KEYS */;
UNLOCK TABLES;
DROP TABLE IF EXISTS `global_notification_settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
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`
@ -126,24 +120,9 @@ CREATE TABLE `metric_names` (
`description` text DEFAULT NULL,
PRIMARY KEY (`id`),
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 */;
--
-- 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`
--
@ -166,15 +145,6 @@ CREATE TABLE `metric_thresholds` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!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`
--
@ -192,18 +162,6 @@ CREATE TABLE `server_groups` (
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!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`
--
@ -222,285 +180,9 @@ CREATE TABLE `server_metrics` (
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_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 */;
--
-- 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`
--
@ -524,18 +206,6 @@ CREATE TABLE `servers` (
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!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`
--
@ -555,80 +225,9 @@ CREATE TABLE `service_alerts` (
PRIMARY KEY (`id`),
KEY `idx_server_service` (`server_id`,`service_name`),
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 */;
--
-- 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`
--
@ -639,7 +238,7 @@ DROP TABLE IF EXISTS `service_status`;
CREATE TABLE `service_status` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`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,
`load_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`),
KEY `idx_server_updated` (`server_id`,`updated_at`),
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 */;
--
-- 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`
--
@ -771,21 +264,15 @@ CREATE TABLE `user_notification_settings` (
`user_id` int(11) NOT NULL,
`telegram_chat_id` varchar(50) 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`),
KEY `user_id` (`user_id`),
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 */;
--
-- 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`
--
@ -802,20 +289,8 @@ CREATE TABLE `users` (
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
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 */;
--
-- 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 */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
@ -826,4 +301,85 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!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

0
public/debug-login.php Normal file → Executable file
View File

11
public/index.php Normal file → Executable file
View File

@ -129,7 +129,7 @@ $dashboardGroup = $app->group('', function ($group) use ($twig) {
$stats = $serverModel->getStats();
// Get servers with latest metrics
$servers = $serverModel->getAll();
$servers = $serverModel->getServersWithStatus();
$templateData = [
'title' => 'Дашборд мониторинга',
@ -161,6 +161,11 @@ $groupsGroup = $app->group('/groups', function ($group) use ($groupController) {
$group->get('/{id}', [$groupController, 'show']);
})->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)
$serversGroup = $app->group('/servers', function ($group) use ($serverController, $serverDetailController) {
$group->get('', [$serverController, 'index']);
@ -186,7 +191,11 @@ $alertsGroup = $app->group('/alerts', function ($group) use ($alertController) {
// Admin routes (protected with auth middleware and csrf)
$adminGroup = $app->group('/admin', function ($group) use ($adminController) {
$group->get('/users', [$adminController, 'usersList']);
$group->post("/users/save", [$adminController, "saveUser"]);
$group->get("/users/{id}/delete", [$adminController, "deleteUser"]);
$group->get('/notifications', [$adminController, 'notificationSettings']);
$group->post("/notifications/save", [$adminController, "saveNotificationSettings"]);
$group->get("/notifications/test", [$adminController, "testNotification"]);
})->add($csrfMiddleware)->add(AuthMiddleware::class);
// API route for agents (public, no auth middleware, no csrf)

0
public/index.php.broken Normal file → Executable file
View File

0
public/login-direct.php Normal file → Executable file
View File

0
public/session_check.php Normal file → Executable file
View File

0
public/session_test.php Normal file → Executable file
View File

0
public/set_session.php Normal file → Executable file
View File

View File

@ -4,6 +4,7 @@
namespace App\Controllers;
use App\Models\Model;
use App\Services\NotificationService;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
@ -11,43 +12,259 @@ use Slim\Views\Twig;
class AdminController extends Model
{
private $twig;
private $notificationService;
public function __construct(Twig $twig)
{
parent::__construct();
$this->twig = $twig;
$this->notificationService = new NotificationService();
}
// ==================== ПОЛЬЗОВАТЕЛИ ====================
public function usersList(Request $request, Response $response, $args)
{
// Только для администраторов
if ($_SESSION['role'] !== 'admin') {
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();
$users = $stmt->fetchAll();
$templateData = [
return $this->twig->render($response, 'admin/users.twig', [
'title' => 'Управление пользователями',
'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') {
return $response->withHeader('Location', '/')->withStatus(302);
}
$templateData = [
'title' => 'Настройки уведомлений'
];
$data = $request->getParsedBody();
$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);
}
}

View File

@ -4,12 +4,21 @@
namespace App\Controllers\Api;
use App\Models\Model;
use App\Services\NotificationService;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Config\DatabaseConfig;
class MetricsController extends Model
{
private $notificationService;
public function __construct()
{
parent::__construct();
$this->notificationService = new NotificationService();
}
public function collectMetrics(Request $request, Response $response, $args)
{
$input = json_decode($request->getBody(), true);
@ -40,6 +49,7 @@ class MetricsController extends Model
}
$serverId = $tokenInfo['server_id'];
$serverName = $tokenInfo['server_name'];
// Обновляем время последних метрик для сервера
$stmt = $this->pdo->prepare("
@ -71,8 +81,8 @@ class MetricsController extends Model
':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
]);
// Если сервис остановлен и включено его мониторинг - создаем алерт
// Если сервис остановлен - создаем алерт
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);
}
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)
{
$serverId = $args['id'];
// Получаем список сервисов
$stmt = $this->pdo->prepare("
SELECT service_name, status, load_state, active_state, sub_state, updated_at
FROM service_status
@ -164,10 +266,12 @@ class MetricsController extends Model
return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Time parameter required']));
}
// Пытаемся распознать различные форматы времени
$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)) {
$today = date('Y-m-d');
$timestamp = strtotime($today . ' ' . $timeParam);
@ -179,54 +283,28 @@ class MetricsController extends Model
$time = date('Y-m-d H:i:s', $timestamp);
// Получаем топ-процессы CPU для указанного времени
$stmt = $this->pdo->prepare("
SELECT value
FROM server_metrics sm
SELECT value FROM server_metrics sm
JOIN metric_names mn ON sm.metric_name_id = mn.id
WHERE sm.server_id = :server_id
AND mn.name = 'top_cpu_proc'
WHERE sm.server_id = :server_id AND mn.name = 'top_cpu_proc'
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))
LIMIT 1
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1
");
$stmt->execute([
':server_id' => $serverId,
':time1' => $time,
':time2' => $time,
':time3' => $time
]);
$stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]);
$topCpuResult = $stmt->fetch();
// Получаем топ-процессы RAM для указанного времени
$stmt = $this->pdo->prepare("
SELECT value
FROM server_metrics sm
SELECT value FROM server_metrics sm
JOIN metric_names mn ON sm.metric_name_id = mn.id
WHERE sm.server_id = :server_id
AND mn.name = 'top_ram_proc'
WHERE sm.server_id = :server_id AND mn.name = 'top_ram_proc'
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))
LIMIT 1
ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1
");
$stmt->execute([
':server_id' => $serverId,
':time1' => $time,
':time2' => $time,
':time3' => $time
]);
$stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]);
$topRamResult = $stmt->fetch();
$topCpu = [];
$topRam = [];
if ($topCpuResult && !empty($topCpuResult['value'])) {
$topCpu = json_decode($topCpuResult['value'], true);
}
if ($topRamResult && !empty($topRamResult['value'])) {
$topRam = json_decode($topRamResult['value'], true);
}
$topCpu = $topCpuResult ? json_decode($topCpuResult['value'], true) : [];
$topRam = $topRamResult ? json_decode($topRamResult['value'], true) : [];
$response->getBody()->write(json_encode([
'top_cpu' => $topCpu,
@ -268,87 +346,12 @@ class MetricsController extends Model
];
}
$data = [
$response->getBody()->write(json_encode([
'server_id' => (int)$serverId,
'from' => $from,
'to' => $to,
'points_count' => count($metrics),
'metrics' => $grouped
];
$response->getBody()->write(json_encode($data));
]));
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
]);
}
}
}

6
src/Controllers/DashboardController.php Normal file → Executable file
View File

@ -23,8 +23,9 @@ class DashboardController
// Получаем статистику
$stats = $this->serverModel->getStats();
// Получаем список серверов с последними метриками
$servers = $this->serverModel->getAll();
// Получаем список серверов со статусами для цветных карточек
$servers = $this->serverModel->getServersWithStatus();
$templateData = [
'title' => 'Дашборд мониторинга',
@ -32,6 +33,7 @@ class DashboardController
'servers' => $servers
];
file_put_contents("/tmp/dashboard_debug.log", json_encode($servers) . "\n", FILE_APPEND);
return $this->twig->render($response, 'dashboard.twig', $templateData);
}
}

0
src/Controllers/GroupController.php Normal file → Executable file
View File

View File

@ -7,6 +7,7 @@ use App\Models\Model;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use DateTime;
class ServerDetailController extends Model
{

0
src/Middlewares/CsrfMiddleware.php Normal file → Executable file
View File

View File

@ -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);
}
}

7
src/Middlewares/SessionMiddleware.php Normal file → Executable file
View File

@ -23,9 +23,14 @@ class SessionMiddleware
$sessionData = [
'user_id' => $_SESSION['user_id'] ?? 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 = $this->twig->getEnvironment();
$environment->addGlobal('session', $sessionData);

0
src/Models/Alert.php Normal file → Executable file
View File

0
src/Models/Group.php Normal file → Executable file
View File

78
src/Models/Server.php Normal file → Executable file
View File

@ -38,22 +38,92 @@ class Server
{
$stats = [];
// Общее количество серверов
$stmt = $this->db->query("SELECT COUNT(*) as total FROM servers");
$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");
$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'");
$stats['warnings'] = $stmt->fetch()['total'];
// Активные алерты (critical)
$stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'critical'");
$stats['criticals'] = $stmt->fetch()['total'];
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;
}
}

View File

@ -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;
}
}

0
src/Utils/EncryptionHelper.php Normal file → Executable file
View File

View File

@ -1,79 +1,197 @@
{% extends "layout.twig" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-bell"></i> Настройки уведомлений</h3>
<div class="row mb-4">
<div class="col-12">
<h2><i class="fas fa-bell"></i> Настройки уведомлений</h2>
<p class="text-muted">Настройте отправку уведомлений через Email и Telegram</p>
</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 class="card-body">
<form method="post" action="/admin/notifications">
<h5><i class="fas fa-envelope"></i> Email уведомления</h5>
<div class="row">
<div class="col-md-6">
<label for="smtp_host" class="form-label">SMTP сервер</label>
<input type="text" class="form-control" id="smtp_host" name="smtp_host" placeholder="smtp.gmail.com">
</div>
<div class="col-md-6">
<label for="smtp_port" class="form-label">Порт</label>
<input type="number" class="form-control" id="smtp_port" name="smtp_port" placeholder="587">
</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)
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="notify_on_warning" id="notify_on_warning"
{% if settings.notify_on_warning %}checked{% endif %}>
<label class="form-check-label" for="notify_on_warning">
<i class="fas fa-exclamation-triangle text-warning"></i>
При предупреждениях (warning)
</label>
</div>
</div>
<div class="mb-4">
<h5><i class="fab fa-telegram"></i> Telegram уведомления</h5>
<div class="row">
<div class="col-md-6">
<label for="telegram_bot_token" class="form-label">Bot Token</label>
<input type="text" class="form-control" id="telegram_bot_token" name="telegram_bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ">
<div class="form-check form-switch">
<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 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 class="mb-4">
<h5><i class="fas fa-mobile-alt"></i> SMS уведомления</h5>
<div class="row">
<div class="col-md-6">
<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">
<!-- Кнопки -->
<div class="row mb-4">
<div class="col-12 d-flex justify-content-between">
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Сохранить настройки
</button>
</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>
</div>
</div>
</div>
</div>
<script>
function toggleSection(id, show) {
document.getElementById(id).style.display = show ? 'block' : 'none';
}
</script>
{% endblock %}

View File

@ -5,11 +5,18 @@
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<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> Добавить пользователя
</a>
</button>
</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-body">
{% if users|length > 0 %}
@ -21,6 +28,8 @@
<th>Имя пользователя</th>
<th>Email</th>
<th>Роль</th>
<th>Telegram Chat ID</th>
<th>Email для алертов</th>
<th>Дата создания</th>
<th>Действия</th>
</tr>
@ -29,7 +38,7 @@
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td><strong>{{ user.username }}</strong></td>
<td>{{ user.email|default('-') }}</td>
<td>
{% if user.role == 'admin' %}
@ -38,16 +47,20 @@
<span class="badge bg-secondary"><i class="fas fa-user"></i> Пользователь</span>
{% endif %}
</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>
<a href="/admin/users/{{ user.id }}/edit" class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit"></i> Редактировать
</a>
<form action="/admin/users/{{ user.id }}" method="post" style="display: inline-block;" onsubmit="return confirm('Вы уверены, что хотите удалить этого пользователя?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i> Удалить
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal" data-bs-target="#userModal"
onclick="openEditModal({{ user.id }}, '{{ user.username }}', '{{ user.email }}', '{{ user.role }}', '{{ user.telegram_chat_id }}', '{{ user.email_for_alerts }}')">
<i class="fas fa-edit"></i>
</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>
</tr>
{% endfor %}
@ -55,16 +68,107 @@
</table>
</div>
{% 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>
<p class="lead">Пользователи пока не созданы</p>
<a href="#" class="btn btn-primary">
<i class="fas fa-plus"></i> Создать первого пользователя
</a>
<p class="lead">Пользователи не найдены</p>
</div>
{% endif %}
</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 %}

View File

@ -1,125 +1,234 @@
{% extends "layout.twig" %}
{% 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="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">
<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>
<p class="text-muted mb-0">Всего серверов</p>
<p class="mb-0">Всего серверов</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card text-white bg-success">
<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>
<p class="text-muted mb-0">С метриками</p>
<p class="mb-0">С метриками</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card text-white bg-warning">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2"></i>
<h3>{{ stats.alerts_count }}</h3>
<p class="text-muted mb-0">Активных алертов</p>
<i class="fas fa-exclamation-triangle fa-2x mb-2"></i>
<h3>{{ stats.warnings }}</h3>
<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>
<!-- Servers List -->
<!-- Карточки серверов -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="fas fa-server"></i> Серверы</h4>
{% for server in servers %}
<div class="col-xl-4 col-lg-6 col-md-6 mb-4">
<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>
<a href="/servers/create" class="btn btn-sm btn-outline-primary me-2">
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить сервер</span>
</a>
<a href="/servers" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-list"></i> <span class="d-none d-sm-inline">Все серверы</span>
</a>
{% if server.status == 'online' %}
<span class="badge bg-light text-dark">
<i class="fas fa-check-circle"></i> Онлайн
</span>
{% elseif server.status == 'warning' %}
<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 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 %}
<div class="mb-2">
<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>
{% 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>
{% 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>
{% 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 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>
{% 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 %}

0
templates/groups/show.twig Normal file → Executable file
View File

7
templates/layout.twig Normal file → Executable file
View File

@ -71,6 +71,13 @@
{% endif %}
<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 %}
</main>

0
templates/login-layout.twig Normal file → Executable file
View File

0
templates/login.twig Normal file → Executable file
View File

View File

@ -90,50 +90,44 @@
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
<div class="row mb-3">
<div class="col-md-12">
<form method="get" class="row g-2 align-items-end" id="periodForm">
<input type="hidden" name="tab" value="metrics">
<!-- Пресеты -->
<div class="col-md-3">
<label class="form-label mb-1">
<i class="fas fa-clock"></i> Быстрый выбор
</label>
<select class="form-select" id="presetSelect" onchange="applyPreset()">
<option value="">-- Выбрать --</option>
<option value="30">Последние 30 минут</option>
<option value="60">Последние 1 час</option>
<option value="120">Последние 2 часа</option>
<option value="360">Последние 6 часов</option>
<option value="720">Последние 12 часов</option>
<option value="1440">Последние 24 часа</option>
</select>
<!-- Отладка: period = {{ request.query.period }} -->
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
<div class="btn-group d-flex" role="group">
<a href="?tab=metrics&amp;period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
24 часа
</a>
<a href="?tab=metrics&amp;period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
7 дней
</a>
<a href="?tab=metrics&amp;period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
30 дней
</a>
</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 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 class="col-md-3">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i> Применить
</button>
<div class="row mt-2">
<div class="col-md-12">
<small class="text-muted">Масштаб:</small>
<div class="btn-group ms-2" role="group">
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=auto" class="btn btn-sm {% if not zoom or zoom == 'auto' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
авто
</a>
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=1h" class="btn btn-sm {% if zoom == '1h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
</a>
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
</a>
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
24ч
</a>
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
</a>
<a href="?tab=metrics&amp;period={{ period }}&amp;zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
30д
</a>
</div>
</form>
</div>
</div>
@ -416,6 +410,7 @@
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/chartjs-plugin-crosshair.min.js"></script>
<script>
// Функция для получения топ-процессов для указанного времени
@ -504,9 +499,9 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
var labels{{ 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_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) }}');
data{{ metricName }}.push({{ metric.value|raw }});
{% endfor %}
@ -524,6 +519,19 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
}]
},
options: {
crosshair: {
line: {
color: "rgba(100, 150, 255, 0.4)",
width: 1,
dashPattern: []
},
sync: {
enabled: false
},
zoom: {
enabled: false
}
},
responsive: true,
maintainAspectRatio: false,
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;
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 time = labels{{ metricName }}[dataIndex];
@ -595,7 +613,6 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
// Show tooltip
var position = context.chart.canvas.getBoundingClientRect();
tooltipEl.innerHTML = lines.join('<br>');
tooltipEl.style.visibility = 'visible';
tooltipEl.style.opacity = 1;
tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX + 10 + '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 в любую сторону
var canvas{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}');
if (canvas{{ metricName|replace({'-': '_', '.': '_'}) }}) {
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = false;
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}.addEventListener('mouseout', function() {
canvas{{ metricName|replace({'-': '_', '.': '_'}) }}._tooltipHidden = true;
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas.addEventListener('mouseleave', function() {
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();
});
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 %}
{% endfor %}
</script>
<script>
// Применение пресета
function applyPreset() {
var select = document.getElementById("presetSelect");
var minutes = parseInt(select.value);
if (!minutes) return;
var now = new Date();
var startDate = new Date(now.getTime() - (minutes * 60 * 1000));
// Форматируем для datetime-local (YYYY-MM-DDTHH:MM)
function formatDateTimeLocal(date) {
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, "0");
var day = String(date.getDate()).padStart(2, "0");
var hours = String(date.getHours()).padStart(2, "0");
var mins = String(date.getMinutes()).padStart(2, "0");
return year + "-" + month + "-" + day + "T" + hours + ":" + mins;
// Глобальный обработчик mousemove для скрытия тултипов при уходе курсора за пределы canvas
// (нужен т.к. crosshair плагин создаёт overlay canvas и перехватывает mouseleave)
document.addEventListener('mousemove', function(e) {
{% for metricName, metricData in metrics %}
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
(function() {
var canvas = chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas;
var rect = canvas.getBoundingClientRect();
var isOverCanvas = (e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom);
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>
{% endblock %}