diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73d3c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +node_modules/ +*.log diff --git a/AGENTS.md b/AGENTS.md old mode 100644 new mode 100755 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md old mode 100644 new mode 100755 diff --git a/composer.json b/composer.json index 387c40b..a4020f9 100755 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "autoload": { "psr-4": { "App\\": "src/", + "App\\Services\\": "src/Services", "Config\\": "config/" } }, diff --git a/monitoring_system_dump.sql b/monitoring_system_dump.sql old mode 100644 new mode 100755 index a7d8a7a..62ae084 --- a/monitoring_system_dump.sql +++ b/monitoring_system_dump.sql @@ -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); diff --git a/public/chartjs-plugin-crosshair.min.js b/public/chartjs-plugin-crosshair.min.js new file mode 100644 index 0000000..643cfd1 --- /dev/null +++ b/public/chartjs-plugin-crosshair.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-crosshair v2.0.0 + * https://chartjs-plugin-crosshair.netlify.com + * (c) 2023 Chart.js Contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Chart,t.Chart.helpers)}(this,(function(t,e){"use strict";var a={line:{color:"#F66",width:1,dashPattern:[]},sync:{enabled:!0,group:1,suppressTooltips:!1},zoom:{enabled:!0,zoomboxBackgroundColor:"rgba(66,133,244,0.2)",zoomboxBorderColor:"#48F",zoomButtonText:"Reset Zoom",zoomButtonClass:"reset-zoom"},snap:{enabled:!1},callbacks:{beforeZoom:function(t,e){return!0},afterZoom:function(t,e){}}},s={id:"crosshair",afterInit:function(t){if(t.config.options.scales.x){var e=t.config.options.scales.x.type;if("linear"===e||"time"===e||"category"===e||"logarithmic"===e)void 0===t.options.plugins.crosshair&&(t.options.plugins.crosshair=a),t.crosshair={enabled:!1,suppressUpdate:!1,x:null,originalData:[],originalXRange:{},dragStarted:!1,dragStartX:null,dragEndX:null,suppressTooltips:!1,ignoreNextEvents:0,reset:function(){this.resetZoom(t,!1,!1)}.bind(this)},this.getOption(t,"sync","enabled")&&(t.crosshair.syncEventHandler=function(e){this.handleSyncEvent(t,e)}.bind(this),t.crosshair.resetZoomEventHandler=function(e){var a=this.getOption(t,"sync","group");e.chartId!==t.id&&e.syncGroup===a&&this.resetZoom(t,!0)}.bind(this),window.addEventListener("sync-event",t.crosshair.syncEventHandler),window.addEventListener("reset-zoom-event",t.crosshair.resetZoomEventHandler)),t.panZoom=this.panZoom.bind(this,t)}},afterDestroy:function(t){this.getOption(t,"sync","enabled")&&(window.removeEventListener("sync-event",t.crosshair.syncEventHandler),window.removeEventListener("reset-zoom-event",t.crosshair.resetZoomEventHandler))},panZoom:function(t,e){if(0!==t.crosshair.originalData.length){var a=t.crosshair.end-t.crosshair.start,s=t.crosshair.min,r=t.crosshair.max;e<0?(t.crosshair.start=Math.max(t.crosshair.start+e,s),t.crosshair.end=t.crosshair.start===s?s+a:t.crosshair.end+e):(t.crosshair.end=Math.min(t.crosshair.end+e,t.crosshair.max),t.crosshair.start=t.crosshair.end===r?r-a:t.crosshair.start+e),this.doZoom(t,t.crosshair.start,t.crosshair.end)}},getOption:function(t,s,r){return e.valueOrDefault(t.options.plugins.crosshair[s]?t.options.plugins.crosshair[s][r]:void 0,a[s][r])},getXScale:function(t){return t.data.datasets.length?t.scales[t.getDatasetMeta(0).xAxisID]:null},getYScale:function(t){return t.scales[t.getDatasetMeta(0).yAxisID]},handleSyncEvent:function(t,e){var a=this.getOption(t,"sync","group");if(e.chartId!==t.id&&e.syncGroup===a){var s=this.getXScale(t);if(s){var r=void 0===e.original.native.buttons?e.original.native.which:e.original.native.buttons;"mouseup"===e.original.type&&(r=0);var o={type:"click"==e.original.type?"mousemove":e.original.type,chart:t,x:s.getPixelForValue(e.xValue),y:e.original.y,native:{buttons:r},stop:!0};t._eventHandler(o)}}},afterEvent:function(t,e){if(0==t.config.options.scales.x.length)return;let a=e.event;var s=t.config.options.scales.x.type;if("linear"===s||"time"===s||"category"===s||"logarithmic"===xscaleType){var r=this.getXScale(t);if(r)if(t.crosshair.ignoreNextEvents>0)t.crosshair.ignoreNextEvents-=1;else{var o=void 0===a.native.buttons?a.native.which:a.native.buttons;"mouseup"===a.native.type&&(o=0);var i=this.getOption(t,"sync","enabled"),n=this.getOption(t,"sync","group");if(!a.stop&&i)(e=new CustomEvent("sync-event")).chartId=t.id,e.syncGroup=n,e.original=a,e.xValue=r.getValueForPixel(a.x),window.dispatchEvent(e);var c=this.getOption(t,"sync","suppressTooltips");if(t.crosshair.suppressTooltips=a.stop&&c,t.crosshair.enabled="mouseout"!==a.type&&a.x>r.getPixelForValue(r.min)&&a.xr.getPixelForValue(r.max)&&(t.crosshair.suppressUpdate=!0,t.update("none")),t.crosshair.dragStarted=!1,!1;t.crosshair.suppressUpdate=!1;var l=this.getOption(t,"zoom","enabled");if(1===o&&!t.crosshair.dragStarted&&l&&(t.crosshair.dragStartX=a.x,t.crosshair.dragStarted=!0),t.crosshair.dragStarted&&0===o){t.crosshair.dragStarted=!1;var h=r.getValueForPixel(t.crosshair.dragStartX),d=r.getValueForPixel(t.crosshair.x);Math.abs(t.crosshair.dragStartX-t.crosshair.x)>1&&this.doZoom(t,h,d),t.update("none")}t.crosshair.x=a.x,t.draw()}}},afterDraw:function(t){if(t.crosshair.enabled)return t.crosshair.dragStarted?this.drawZoombox(t):(this.drawTraceLine(t),this.interpolateValues(t),this.drawTracePoints(t)),!0},beforeTooltipDraw:function(t){return!t.crosshair.dragStarted&&!t.crosshair.suppressTooltips},resetZoom:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],a=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if(a){if(t.crosshair.originalData.length>0)for(var s=0;sr){var o=s;s=r,r=o}if(!e.valueOrDefault(t.options.plugins.crosshair.callbacks?t.options.plugins.crosshair.callbacks.beforeZoom:void 0,a.callbacks.beforeZoom)(s,r))return!1;if(t.crosshair.dragStarted=!1,t.options.scales.x.min&&0===t.crosshair.originalData.length&&(t.crosshair.originalXRange.min=t.options.scales.x.min),t.options.scales.x.max&&0===t.crosshair.originalData.length&&(t.crosshair.originalXRange.max=t.options.scales.x.max),!t.crosshair.button){var i=document.createElement("button"),n=this.getOption(t,"zoom","zoomButtonText"),c=this.getOption(t,"zoom","zoomButtonClass"),l=document.createTextNode(n);i.appendChild(l),i.className=c,i.addEventListener("click",function(){this.resetZoom(t)}.bind(this)),t.canvas.parentNode.appendChild(i),t.crosshair.button=i}t.options.scales.x.min=s,t.options.scales.x.max=r;var h=0===t.crosshair.originalData.length;if("category"!==t.config.options.scales.x.type)for(var d=0;d=s&&!u&&p>0&&(g.push(f[p-1]),u=!0),y>=s&&y<=r&&g.push(v),y>r&&!x&&p=r})),n=o[i-1],c=o[i];if(t.data.datasets[e].steppedLine&&n)a.interpolatedValue=n.y;else if(n&&c){var l=(c.y-n.y)/(c.x-n.x);a.interpolatedValue=n.y+(r-n.x)*l}else a.interpolatedValue=NaN}}}};t.Chart.register(s),t.Interaction.modes.interpolate=function(e,a,s){for(var r=[],o=0;on.max||l=l}));if(-1!==d){var g=h[d-1],p=h[d];if(g&&p)var u=(p.y-g.y)/(p.x-g.x),x=g.y+(l-g.x)*u;if(e.data.datasets[o].steppedLine&&g&&(x=g.y),!isNaN(x)){var f=c.getPixelForValue(x);if(!isNaN(f)){var m={hasValue:function(){return!0},tooltipPosition:function(){return this._model},_model:{x:a.x,y:f},skip:!1,stop:!1,x:l,y:x};r.push({datasetIndex:o,element:m,index:0})}}}}}}var v=t.Interaction.modes.x(e,a,s);for(d=0;dgroup('', 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) diff --git a/public/index.php.broken b/public/index.php.broken old mode 100644 new mode 100755 diff --git a/public/login-direct.php b/public/login-direct.php old mode 100644 new mode 100755 diff --git a/public/session_check.php b/public/session_check.php old mode 100644 new mode 100755 diff --git a/public/session_test.php b/public/session_test.php old mode 100644 new mode 100755 diff --git a/public/set_session.php b/public/set_session.php old mode 100644 new mode 100755 diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index 4d2b287..38c77a1 100755 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -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); } -} \ No newline at end of file + + 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); + } +} diff --git a/src/Controllers/Api/MetricsController.php b/src/Controllers/Api/MetricsController.php index 4751b87..ad31381 100755 --- a/src/Controllers/Api/MetricsController.php +++ b/src/Controllers/Api/MetricsController.php @@ -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,69 +266,45 @@ class MetricsController extends Model return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Time parameter required'])); } - // Пытаемся распознать различные форматы времени $timestamp = strtotime($timeParam); - - // Если время только в формате HH:MM, добавляем сегодняшнюю дату + // Парсинг формата 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"); + } + if ($timestamp === false && preg_match('/^\d{1,2}:\d{2}$/', $timeParam)) { $today = date('Y-m-d'); $timestamp = strtotime($today . ' ' . $timeParam); } - + if ($timestamp === false) { return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Invalid time format'])); } $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, @@ -242,7 +320,7 @@ class MetricsController extends Model $serverId = $args['id']; $from = $request->getQueryParams()['from'] ?? date('Y-m-d H:i:s', strtotime('-24 hours')); $to = $request->getQueryParams()['to'] ?? date('Y-m-d H:i:s'); - + $stmt = $this->pdo->prepare(" SELECT sm.value, mn.name, mn.unit, sm.created_at FROM server_metrics sm @@ -254,7 +332,7 @@ class MetricsController extends Model "); $stmt->execute([':id' => $serverId, ':from' => $from, ':to' => $to]); $metrics = $stmt->fetchAll(); - + $grouped = []; foreach ($metrics as $m) { $name = $m['name']; @@ -267,88 +345,13 @@ class MetricsController extends Model 'unit' => $m['unit'] ]; } - - $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 - ]); - } - } } diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php old mode 100644 new mode 100755 index 88833cb..78df41c --- a/src/Controllers/DashboardController.php +++ b/src/Controllers/DashboardController.php @@ -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); } -} \ No newline at end of file +} diff --git a/src/Controllers/GroupController.php b/src/Controllers/GroupController.php old mode 100644 new mode 100755 diff --git a/src/Controllers/ServerDetailController.php b/src/Controllers/ServerDetailController.php index e8bef74..40b25a5 100755 --- a/src/Controllers/ServerDetailController.php +++ b/src/Controllers/ServerDetailController.php @@ -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 { diff --git a/src/Middlewares/CsrfMiddleware.php b/src/Middlewares/CsrfMiddleware.php old mode 100644 new mode 100755 diff --git a/src/Middlewares/FlashMiddleware.php b/src/Middlewares/FlashMiddleware.php new file mode 100755 index 0000000..9a47ba8 --- /dev/null +++ b/src/Middlewares/FlashMiddleware.php @@ -0,0 +1,23 @@ +handle($request); + } +} diff --git a/src/Middlewares/SessionMiddleware.php b/src/Middlewares/SessionMiddleware.php old mode 100644 new mode 100755 index 4de4317..3fde29d --- a/src/Middlewares/SessionMiddleware.php +++ b/src/Middlewares/SessionMiddleware.php @@ -23,13 +23,18 @@ 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); return $handler->handle($request); } -} \ No newline at end of file +} diff --git a/src/Models/Alert.php b/src/Models/Alert.php old mode 100644 new mode 100755 diff --git a/src/Models/Group.php b/src/Models/Group.php old mode 100644 new mode 100755 diff --git a/src/Models/Server.php b/src/Models/Server.php old mode 100644 new mode 100755 index a926c22..d1310a3 --- a/src/Models/Server.php +++ b/src/Models/Server.php @@ -17,18 +17,18 @@ class Server public function getAll() { - $stmt = $this->db->query("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color - FROM servers s - LEFT JOIN server_groups sg ON s.group_id = sg.id + $stmt = $this->db->query("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id ORDER BY s.name"); return $stmt->fetchAll(); } public function getById($id) { - $stmt = $this->db->prepare("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color - FROM servers s - LEFT JOIN server_groups sg ON s.group_id = sg.id + $stmt = $this->db->prepare("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id WHERE s.id = ?"); $stmt->execute([$id]); return $stmt->fetch(); @@ -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; } -} \ No newline at end of file + + /** + * Получить серверы с вычисленным статусом + * 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; + } +} diff --git a/src/Services/NotificationService.php b/src/Services/NotificationService.php new file mode 100755 index 0000000..2351264 --- /dev/null +++ b/src/Services/NotificationService.php @@ -0,0 +1,272 @@ +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 = "{$subject}\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; + } +} diff --git a/src/Utils/EncryptionHelper.php b/src/Utils/EncryptionHelper.php old mode 100644 new mode 100755 diff --git a/templates/admin/notifications.twig b/templates/admin/notifications.twig index 8b13074..3e5941d 100755 --- a/templates/admin/notifications.twig +++ b/templates/admin/notifications.twig @@ -1,79 +1,197 @@ {% extends "layout.twig" %} {% block content %} -
-
-
-
-

Настройки уведомлений

+
+
+

Настройки уведомлений

+

Настройте отправку уведомлений через Email и Telegram

+
+
+ +{% if session.flash_message is defined and session.flash_message %} +
+ {{ session.flash_message|nl2br }} + +
+{% endif %} + +
+ + + +
+ +
+
+
+
Email (SMTP)
+
+
+
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
-
- -
Email уведомления
-
-
- - -
-
- - -
+
+ + +
+
+
+
Telegram Bot
+
+
+
+ + +
+ +
+
+ + +
Получите у @BotFather в Telegram
-
-
- - -
-
- - -
+ +
+ + +
ID чата или группы (отрицательное число для групп)
-
- - + +
+ + +
Оставьте пустым, если прокси не нужен
+
+ +
+ + Как настроить: +
    +
  1. Создайте бота через @BotFather
  2. +
  3. Добавьте бота в чат/группу
  4. +
  5. Отправьте сообщение в чат
  6. +
  7. Узнайте Chat ID через https://api.telegram.org/bot[TOKEN]/getUpdates
  8. +
- -
-
Telegram уведомления
-
-
- - -
-
- - -
-
-
- -
-
SMS уведомления
-
-
- - -
-
- - -
-
-
- -
- -
- +
-
-{% endblock %} \ No newline at end of file + + +
+
+
+
+
Когда отправлять уведомления
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+ +
+ +
+
+ + + + +{% endblock %} diff --git a/templates/admin/users.twig b/templates/admin/users.twig index 4d51c6a..24f8e3a 100755 --- a/templates/admin/users.twig +++ b/templates/admin/users.twig @@ -5,11 +5,18 @@

Управление пользователями

- +
+ {% if session.flash_message is defined and session.flash_message %} +
+ {{ session.flash_message|nl2br }} + +
+ {% endif %} +
{% if users|length > 0 %} @@ -21,6 +28,8 @@ Имя пользователя Email Роль + Telegram Chat ID + Email для алертов Дата создания Действия @@ -29,7 +38,7 @@ {% for user in users %} {{ user.id }} - {{ user.username }} + {{ user.username }} {{ user.email|default('-') }} {% if user.role == 'admin' %} @@ -38,16 +47,20 @@ Пользователь {% endif %} - {{ user.created_at|date('d.m.Y H:i:s') }} + {{ user.telegram_chat_id|default('-') }} + {{ user.email_for_alerts|default('-') }} + {{ user.created_at|date('d.m.Y H:i') }} - - Редактировать + + + -
- -
{% endfor %} @@ -55,16 +68,107 @@
{% else %} -
+
-

Пользователи пока не созданы

- - Создать первого пользователя - +

Пользователи не найдены

{% endif %}
-{% endblock %} \ No newline at end of file + + + + + + +{% endblock %} diff --git a/templates/dashboard.twig b/templates/dashboard.twig index c83fb53..8ee40a6 100755 --- a/templates/dashboard.twig +++ b/templates/dashboard.twig @@ -1,125 +1,234 @@ {% extends "layout.twig" %} {% block content %} -
-
-

Дашборд мониторинга

-
-
- -
-
-
+ +
+

Дашборд мониторинга

+ +
+
+ + +
+
+
- +

{{ stats.total_servers }}

-

Всего серверов

+

Всего серверов

-
-
+
+
- +

{{ stats.servers_with_metrics }}

-

С метриками

+

С метриками

-
-
+
+
- -

{{ stats.alerts_count }}

-

Активных алертов

+ +

{{ stats.warnings }}

+

Предупреждения

+
+
+
+
+
+
+ +

{{ stats.criticals }}

+

Критические

- +
-
-
-
-

Серверы

+ {% for server in servers %} + +
+
+
+
+ {{ server.name }} +
- - Добавить сервер - - - Все серверы - + {% if server.status == 'online' %} + + Онлайн + + {% elseif server.status == 'warning' %} + + Внимание + + {% else %} + + Оффлайн + + {% endif %}
- {% if servers|length > 0 %} -
- - - - - - - - - - - - - {% for server in servers %} - - - - - - - - - {% endfor %} - -
НазваниеАдресГруппаСтатусПоследние метрикиДействия
{{ server.name }}{{ server.address|default('-') }} - {% if server.group_name %} - - {{ server.group_name }} - - {% else %} - Без группы - {% endif %} - - {% if server.last_metrics_at %} - Активен - {% else %} - Нет метрик - {% endif %} - - {% if server.last_metrics_at %} - {{ server.last_metrics_at|date('d.m.Y H:i:s') }} - {% else %} - - - {% endif %} - - - Просмотр - - - Редактировать - -
-
- {% else %} -
- -

Серверы пока не добавлены

- - Добавить первый сервер - + {% if server.group_name %} +
+ + {{ server.group_name }} +
{% endif %} + + {% if server.description %} +

{{ server.description }}

+ {% endif %} + +
Статус: {{ server.status }}
+ + +
+ {% if server.latest_metrics['cpu_load'] is defined %} +
+
+ CPU + {{ server.latest_metrics['cpu_load'].value }}{{ server.latest_metrics['cpu_load'].unit }} +
+
+
+
+
+ {% endif %} + + {% if server.latest_metrics['ram_used'] is defined %} +
+
+ RAM + {{ server.latest_metrics['ram_used'].value }}{{ server.latest_metrics['ram_used'].unit }} +
+
+
+
+
+ {% endif %} + + {% if server.latest_metrics['disk_used'] is defined %} +
+
+ Диск + {{ server.latest_metrics['disk_used'].value }}{{ server.latest_metrics['disk_used'].unit }} +
+
+
+
+
+ {% endif %} +
+ + {% if server.active_alerts > 0 %} +
+ + + Активных алертов: {{ server.active_alerts }} + +
+ {% endif %} + + +
+ + {% if server.last_metrics_at %} + Обновлено: {{ server.last_metrics_at|date('d.m.Y H:i:s') }} + {% else %} + Метрики не получены + {% endif %} +
+
+
+ {% else %} +
+
+
+ +

Серверы пока не добавлены

+

Добавьте первый сервер, чтобы начать мониторинг

+ + Добавить первый сервер + +
+
+
+ {% endfor %}
+ + + + + {% endblock %} diff --git a/templates/groups/show.twig b/templates/groups/show.twig old mode 100644 new mode 100755 diff --git a/templates/layout.twig b/templates/layout.twig old mode 100644 new mode 100755 index 57e5062..669540a --- a/templates/layout.twig +++ b/templates/layout.twig @@ -71,6 +71,13 @@ {% endif %}
+{% if session.flash_message is defined and session.flash_message %} + +{% endif %} + {% block content %}{% endblock %}
diff --git a/templates/login-layout.twig b/templates/login-layout.twig old mode 100644 new mode 100755 diff --git a/templates/login.twig b/templates/login.twig old mode 100644 new mode 100755 diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig index 370e244..b0bf5e5 100755 --- a/templates/servers/detail.twig +++ b/templates/servers/detail.twig @@ -90,50 +90,44 @@
-
- - - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
-
+ + + +
+
+ @@ -416,6 +410,7 @@ + - +// Глобальный обработчик 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(); + } + } + })(); +{% endif %} +{% endfor %} +}); + {% endblock %}