Initial commit - mon.mirv.top monitoring system
This commit is contained in:
commit
c7fdaa5660
|
|
@ -0,0 +1,124 @@
|
||||||
|
# AGENTS.md - Инструкции для агентов по работе с проектом мониторинга
|
||||||
|
|
||||||
|
## О проекте
|
||||||
|
|
||||||
|
**Название:** Система мониторинга серверов
|
||||||
|
**URL:** https://mon.mirv.top
|
||||||
|
**Расположение:** /var/www/mon
|
||||||
|
**Технологии:** PHP 8.1+, Slim Framework 4, Twig, MySQL/MariaDB
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/mon/
|
||||||
|
├── composer.json # Зависимости проекта
|
||||||
|
├── composer.lock # Зафиксированные версии зависимостей
|
||||||
|
├── .env # Конфигурация окружения
|
||||||
|
├── public/
|
||||||
|
│ ├── index.php # Точка входа
|
||||||
|
│ ├── css/ # Стили
|
||||||
|
│ └── js/ # JavaScript
|
||||||
|
├── src/
|
||||||
|
│ ├── Controllers/ # Контроллеры
|
||||||
|
│ ├── Middleware/ # Промежуточное ПО
|
||||||
|
│ ├── Models/ # Модели данных
|
||||||
|
│ └── Services/ # Бизнес-логика
|
||||||
|
├── templates/ # Шаблоны Twig
|
||||||
|
├── migrations/ # Миграции базы данных
|
||||||
|
└── tests/ # Тесты
|
||||||
|
```
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
- PHP 8.1+
|
||||||
|
- Slim Framework 4
|
||||||
|
- Twig template engine
|
||||||
|
- MySQL/MariaDB
|
||||||
|
- Composer для управления зависимостями
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Файл `.env` содержит:
|
||||||
|
- DATABASE_URL - строка подключения к базе данных
|
||||||
|
- JWT_SECRET - секрет для JWT токенов
|
||||||
|
- SMTP настройки - для отправки уведомлений
|
||||||
|
- API токены для агентов мониторинга
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
|
- `GET /api/servers` - список мониторинговых серверов
|
||||||
|
- `GET /api/servers/{id}` - детали сервера
|
||||||
|
- `POST /api/servers` - создание нового сервера
|
||||||
|
- `PUT /api/servers/{id}` - обновление сервера
|
||||||
|
- `DELETE /api/servers/{id}` - удаление сервера
|
||||||
|
- `GET /api/metrics/{server_id}` - метрики сервера
|
||||||
|
- `POST /api/agent/metrics` - получение метрик от агента
|
||||||
|
- `GET /csrf-token` - получение CSRF токена для форм
|
||||||
|
|
||||||
|
## Агентские задачи
|
||||||
|
|
||||||
|
### 1. Добавление нового сервера для мониторинга
|
||||||
|
|
||||||
|
1. Использовать форму в `/servers/create`
|
||||||
|
2. Убедиться, что CSRF токен добавлен к форме (через `/csrf-token`)
|
||||||
|
3. Заполнить поля: название, IP-адрес, порт, токен агента
|
||||||
|
4. Проверить, что сервер отвечает на запросы
|
||||||
|
|
||||||
|
### 2. Обновление конфигурации уведомлений
|
||||||
|
|
||||||
|
1. Перейти в настройки уведомлений
|
||||||
|
2. Обновить email или webhook URL
|
||||||
|
3. Проверить доставку уведомлений
|
||||||
|
|
||||||
|
### 3. Управление группами серверов
|
||||||
|
|
||||||
|
1. Использовать `/groups` для создания/редактирования групп
|
||||||
|
2. Назначать серверы в группы
|
||||||
|
3. Устанавливать правила уведомлений на уровне групп
|
||||||
|
|
||||||
|
### 4. Обновление системы
|
||||||
|
|
||||||
|
1. Сделать резервную копию базы данных
|
||||||
|
2. Обновить зависимости через composer
|
||||||
|
3. Выполнить миграции базы данных
|
||||||
|
4. Обновить конфигурацию nginx при необходимости
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Все формы требуют CSRF токены
|
||||||
|
- Аутентификация через сессии
|
||||||
|
- Валидация входных данных
|
||||||
|
- Санитизация вывода в шаблонах
|
||||||
|
|
||||||
|
## Инструменты для агентов
|
||||||
|
|
||||||
|
- Использовать `composer` для управления зависимостями
|
||||||
|
- Запускать миграции через `php vendor/bin/phinx`
|
||||||
|
- Тестировать через `phpunit` при наличии
|
||||||
|
|
||||||
|
## Часто используемые команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка зависимостей
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# Обновление зависимостей
|
||||||
|
composer update
|
||||||
|
|
||||||
|
# Выполнение миграций
|
||||||
|
php vendor/bin/phinx migrate
|
||||||
|
|
||||||
|
# Запуск тестов
|
||||||
|
php vendor/bin/phpunit
|
||||||
|
|
||||||
|
# Резервное копирование БД
|
||||||
|
mysqldump -u username -p database_name > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Особенности архитектуры
|
||||||
|
|
||||||
|
- MVC паттерн с использованием Slim Framework
|
||||||
|
- Аутентификация через сессии
|
||||||
|
- Middleware для защиты маршрутов
|
||||||
|
- Twig для безопасного рендеринга шаблонов
|
||||||
|
- Поддержка агентов мониторинга через API
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Установка и запуск системы мониторинга
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- PHP 8.1 или выше
|
||||||
|
- Composer
|
||||||
|
- MySQL 8+ или MariaDB 10.5+
|
||||||
|
- Apache или Nginx
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### 1. Клонирование проекта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd monitoring-system
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Настройка базы данных
|
||||||
|
|
||||||
|
1. Создайте базу данных:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE monitoring_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Импортируйте схему:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u root -p monitoring_system < schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Настройка веб-сервера
|
||||||
|
|
||||||
|
#### Apache
|
||||||
|
|
||||||
|
Создайте виртуальный хост:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName mon.mirv.top
|
||||||
|
DocumentRoot /path/to/monitoring-system/public
|
||||||
|
|
||||||
|
<Directory /path/to/monitoring-system/public>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/mon_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/mon_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nginx
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name mon.mirv.top;
|
||||||
|
root /path/to/monitoring-system/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Настройка конфигурации базы данных
|
||||||
|
|
||||||
|
Отредактируйте файл `config/DatabaseConfig.php` для указания параметров подключения к базе данных:
|
||||||
|
|
||||||
|
```php
|
||||||
|
private $host = 'localhost';
|
||||||
|
private $db_name = 'monitoring_system';
|
||||||
|
private $username = 'your_db_username';
|
||||||
|
private $password = 'your_db_password';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### 1. Вход в систему
|
||||||
|
|
||||||
|
Перейдите на `http://mon.mirv.top/login` и войдите в систему.
|
||||||
|
|
||||||
|
Для первоначальной настройки создайте администратора через SQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO users (username, password_hash, email, role)
|
||||||
|
VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@example.com', 'admin');
|
||||||
|
```
|
||||||
|
|
||||||
|
Пароль: `password`
|
||||||
|
|
||||||
|
### 2. Добавление серверов
|
||||||
|
|
||||||
|
1. Перейдите в раздел "Серверы"
|
||||||
|
2. Нажмите "Добавить сервер"
|
||||||
|
3. Заполните форму и сохраните
|
||||||
|
4. На странице подтверждения скопируйте токен и скачайте скрипт установки
|
||||||
|
|
||||||
|
### 3. Установка агента на мониторимый сервер
|
||||||
|
|
||||||
|
1. Скачайте скрипт установки с сервера мониторинга
|
||||||
|
2. Загрузите его на сервер, который нужно мониторить
|
||||||
|
3. Выполните:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x install.sh
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Агент будет установлен как systemd-сервис и начнет отправлять метрики на сервер мониторинга.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Отправка метрик
|
||||||
|
|
||||||
|
Агенты отправляют метрики на эндпоинт `/api/v1/metrics` методом POST:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "токен_сервера",
|
||||||
|
"metrics": {
|
||||||
|
"cpu_load": 45.2,
|
||||||
|
"ram_used": 89.1,
|
||||||
|
"disk_used": 65.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Все пароли хешируются с помощью `password_hash()`
|
||||||
|
- Токены агентов хранятся в виде SHA-256 хешей
|
||||||
|
- Все SQL-запросы используют подготовленные выражения
|
||||||
|
- Вход на все страницы требует аутентификации (кроме API и страницы входа)
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Система мониторинга серверов
|
||||||
|
|
||||||
|
## 1. Общие требования
|
||||||
|
- **Язык:** PHP 8.1+
|
||||||
|
- **Фреймворк:** Slim Framework 4 (с PSR-7, Twig)
|
||||||
|
- **Фронтенд:** Bootstrap 5, Font Awesome 6 (через CDN), Chart.js для графиков
|
||||||
|
- **База данных:** MySQL 8+ / MariaDB 10.5+
|
||||||
|
- **Архитектура:** MVC в рамках Slim (Controllers, Models, Templates)
|
||||||
|
- **Интерфейс:** **Полностью на русском языке** (все надписи, кнопки, формы, меню)
|
||||||
|
- **Безопасность:** Prepared Statements для всех SQL-запросов, хеши паролей (`password_hash`), **токены агентов хранятся только как SHA-256 хеши**.
|
||||||
|
|
||||||
|
## 2. Структура базы данных (8 таблиц)
|
||||||
|
| Таблица | Поля (ключевые) | Назначение |
|
||||||
|
| ------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| **users** | `id, username (unique), password_hash, email, role(admin/user), created_at` | Управление доступом |
|
||||||
|
| **user_notification_settings** | `id, user_id, telegram_chat_id, email_for_alerts` | Персональные настройки уведомлений |
|
||||||
|
| **server_groups** | `id, name, description, icon, color` | Группировка серверов |
|
||||||
|
| **servers** | `id, name, address (nullable), group_id, description, last_metrics_at, created_at` | Основные данные серверов |
|
||||||
|
| **metric_names** | `id, name (unique), unit, description` | Справочник метрик (cpu_load, ram_used, …) |
|
||||||
|
| **metric_thresholds** | `id, server_id, metric_name_id, warning_threshold, critical_threshold, duration` | **Индивидуальные пороги** для каждой метрики на каждом сервере |
|
||||||
|
| **server_metrics** | `id, server_id, metric_name_id, value, created_at` | **Исторические данные** метрик (с индексом `(server_id, metric_name_id, created_at)`) |
|
||||||
|
| **agent_tokens** | `id, server_id (unique), token_hash, created_at, last_used_at` | **Хеши** токенов для авторизации агентов |
|
||||||
|
| **alerts** | `id, server_id, metric_name, value, severity(warning/critical), resolved, created_at, resolved_at` | История срабатывания алертов |
|
||||||
|
|
||||||
|
## 3. Функциональные модули (что должна делать система)
|
||||||
|
|
||||||
|
### 3.1. Аутентификация и авторизация
|
||||||
|
- Доступ ко всем страницам, кроме `/login` и `/api/v1/metrics`, **только после входа**.
|
||||||
|
- Сессионная авторизация с middleware.
|
||||||
|
|
||||||
|
### 3.2. Дашборд (главная страница `/`)
|
||||||
|
- **Цветные карточки серверов** (Bootstrap cards):
|
||||||
|
- **Зеленый:** `last_metrics_at` < 2 мин назад И все метрики ниже порогов.
|
||||||
|
- **Желтый:** метрики свежие (<2 мин), но есть превышение порогов.
|
||||||
|
- **Красный:** `last_metrics_at` > 5 мин назад (сервер «молчит»).
|
||||||
|
- В карточке: имя сервера, текущие значения CPU/RAM, время последнего обновления.
|
||||||
|
- **Автообновление** каждые 30 секунд (через `setTimeout`).
|
||||||
|
|
||||||
|
### 3.3. Управление серверами
|
||||||
|
- **Добавление сервера:** форма с полями (имя, адрес опционально, группа, описание).
|
||||||
|
- **После сохранения:**
|
||||||
|
1. Генерация **уникального токена** (32 символа).
|
||||||
|
2. Сохранение **хеша токена** в `agent_tokens`.
|
||||||
|
3. Показ пользователю: **«Токен для агента: [токен]. Скачайте скрипт установки: [install.sh?token=...]»**.
|
||||||
|
- **Редактирование сервера:** возможность изменить все поля, **повторно скачать скрипт** (токен остается прежним).
|
||||||
|
|
||||||
|
### 3.4. API для агентов
|
||||||
|
- **Эндпоинт:** `POST /api/v1/metrics` (публичный, без авторизации сессии).
|
||||||
|
- **Ожидаемый JSON:** `{"token": "...", "metrics": {"cpu_load": 45.2, "ram_used": 89.1}}`.
|
||||||
|
- **Логика:**
|
||||||
|
1. Проверка токена (сравнение хеша).
|
||||||
|
2. Обновление `servers.last_metrics_at` и `agent_tokens.last_used_at`.
|
||||||
|
3. Сохранение каждой метрики в `server_metrics`.
|
||||||
|
4. **Проверка порогов:** если значение превышает `warning_threshold` из `metric_thresholds` → создание записи в `alerts`.
|
||||||
|
|
||||||
|
### 3.5. Агент мониторинга
|
||||||
|
- **Скрипт установки:** `GET /agent/install.sh?token=...` → динамически генерирует **bash-скрипт**, который:
|
||||||
|
1. Устанавливает Python3, psutil.
|
||||||
|
2. Скачивает Python-агента (`agent.py`) с вашего сервера.
|
||||||
|
3. Подставляет **токен** и **URL API** в конфиг агента.
|
||||||
|
4. Создает systemd-сервис для автостарта.
|
||||||
|
- **Python-агент:** собирает CPU, RAM, Disk раз в **10 секунд**, отправляет на `/api/v1/metrics`.
|
||||||
|
|
||||||
|
### 3.6. Страница сервера (`/server/{id}`)
|
||||||
|
- **Текущие значения** всех метрик.
|
||||||
|
- **Графики Chart.js** для метрик:
|
||||||
|
- По умолчанию — **последние 24 часа**.
|
||||||
|
- Кнопки выбора периода: **24 часа, 7 дней, 30 дней** (через параметр `?period=7d`).
|
||||||
|
- **Управление порогами:** форма настройки `warning_threshold` и `duration` для каждой метрики этого сервера (сохраняет в `metric_thresholds`).
|
||||||
|
|
||||||
|
### 3.7. Система алертов
|
||||||
|
- **Автоматическое создание** при превышении порога.
|
||||||
|
- **Страница `/alerts`:** список активных алертов с кнопкой **«Исправлено»** (помечает `resolved=1`).
|
||||||
|
- **Логика статуса на дашборде** учитывает только **неисправленные (resolved=0)** алерты.
|
||||||
|
|
||||||
|
### 3.8. Уведомления
|
||||||
|
- **Email:** отправка через SMTP (PHPMailer) при срабатывании алерта.
|
||||||
|
- **Telegram Bot:** отправка сообщения в заданный чат.
|
||||||
|
- **Настройка:** в админке (`/admin/notifications`) указываются: SMTP параметры, Telegram Bot Token, Chat ID.
|
||||||
|
|
||||||
|
### 3.9. Администрирование
|
||||||
|
- **CRUD пользователей:** страница `/admin/users` (только для `role='admin'`).
|
||||||
|
- **Управление группами серверов.**
|
||||||
|
- **Повторная выдача скрипта:** на странице сервера кнопка **«Скачать скрипт агента»**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## План поэтапной реализации (9 этапов)
|
||||||
|
|
||||||
|
### Этап 1: Ядро системы
|
||||||
|
- **Цель:** Slim Framework + Twig + Bootstrap работают.
|
||||||
|
- **Результат:** Открываю `http://localhost:8080/test` → вижу «Система мониторинга».
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 2: База данных
|
||||||
|
- **Цель:** Все 8 таблиц созданы.
|
||||||
|
- **Результат:** Импорт `schema.sql` → в MySQL есть все таблицы, включая `metric_names` с записями «cpu_load», «ram_used».
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 3: Аутентификация + CRUD групп
|
||||||
|
- **Цель:** Вход/выход, управление группами.
|
||||||
|
- **Результат:**
|
||||||
|
1. Неавторизован → редирект на `/login` (форма на русском).
|
||||||
|
2. Вход как `admin:123` → переход на дашборд.
|
||||||
|
3. Меню «Группы» → создаю группу «Веб-серверы» → она отображается в списке.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 4: CRUD серверов + генерация токена
|
||||||
|
- **Цель:** Добавление сервера с получением токена.
|
||||||
|
- **Результат:**
|
||||||
|
1. Форма «Добавить сервер» → заполняю → нажимаю «Сохранить».
|
||||||
|
2. Вижу страницу: **«Сервер добавлен. Токен: abc123... Скачайте скрипт: [install.sh?token=abc123...]»**.
|
||||||
|
3. Перехожу по ссылке → скачивается `install.sh`.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 5: API + логика статусов
|
||||||
|
- **Цель:** Приём метрик, цветные карточки на дашборде.
|
||||||
|
- **Результат:**
|
||||||
|
1. Через `curl` отправляю метрики с токеном.
|
||||||
|
2. Обновляю дашборд → вижу карточку сервера **зелёного цвета** (метрики в норме).
|
||||||
|
3. Отправляю `cpu_load: 95` → карточка становится **жёлтой**.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 6: Агент и скрипт установки
|
||||||
|
- **Цель:** Рабочий агент, отправляющий метрики каждые 10 сек.
|
||||||
|
- **Результат:**
|
||||||
|
1. В виртуальной машине запускаю `bash install.sh` → устанавливается Python-агент.
|
||||||
|
2. `systemctl status server-mon-agent` → сервис работает.
|
||||||
|
3. В логах агента вижу: «Отправлено: cpu_load=12.3» каждые 10 сек.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 7: Детали сервера + графики
|
||||||
|
- **Цель:** Страница с графиками и выбором периода.
|
||||||
|
- **Результат:**
|
||||||
|
1. Кликаю на карточку сервера → открывается `/server/1`.
|
||||||
|
2. Вижу график CPU за 24 часа.
|
||||||
|
3. Нажимаю «7 дней» → график перерисовывается за неделю.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 8: Пороги и алерты
|
||||||
|
- **Цель:** Настройка порогов, срабатывание алертов.
|
||||||
|
- **Результат:**
|
||||||
|
1. На странице сервера ставлю порог CPU: 80%.
|
||||||
|
2. Отправляю метрику `cpu_load: 90` → в таблице `alerts` появляется запись.
|
||||||
|
3. Страница `/alerts` показывает «Превышение CPU на сервере X».
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
### Этап 9: Уведомления + админка
|
||||||
|
- **Цель:** Отправка email/Telegram, управление пользователями.
|
||||||
|
- **Результат:**
|
||||||
|
1. В админке указываю свой Telegram Chat ID.
|
||||||
|
2. При срабатывании алерта получаю сообщение в Telegram.
|
||||||
|
3. В меню «Админка» → «Пользователи» создаю нового пользователя.
|
||||||
|
- ✅ **Выполнен**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## История реализации
|
||||||
|
|
||||||
|
### Выполненные этапы:
|
||||||
|
- ✅ Этап 1: Ядро системы (Slim Framework 4, Twig, Bootstrap 5, Font Awesome 6)
|
||||||
|
- ✅ Этап 2: База данных (схема создана, 8 таблиц, стандартные метрики)
|
||||||
|
- ✅ Этап 3: Аутентификация и авторизация (middleware, формы входа/выхода, CRUD групп)
|
||||||
|
- ✅ Этап 4: CRUD серверов (добавление/редактирование/удаление, генерация токенов)
|
||||||
|
- ✅ Этап 5: API для агентов (прием метрик, проверка токенов, обновление статусов)
|
||||||
|
- ✅ Этап 6: Скрипт установки агента (динамическая генерация bash-скрипта)
|
||||||
|
- ✅ Этап 7: Детали сервера и графики (страница с метриками и графиками Chart.js)
|
||||||
|
- ✅ Этап 8: Система алертов (обнаружение превышений, страница алертов)
|
||||||
|
- ✅ Этап 9: Администрирование (управление пользователями, настройки уведомлений)
|
||||||
|
|
||||||
|
### Архитектура:
|
||||||
|
- **Frontend:** Bootstrap 5, Font Awesome 6, Chart.js (через CDN)
|
||||||
|
- **Backend:** Slim Framework 4, Twig templates, PSR-7
|
||||||
|
- **Database:** MySQL/MariaDB с 8 таблицами
|
||||||
|
- **Security:** Password hashing, SHA-256 token hashes, prepared statements
|
||||||
|
- **Language:** Полный перевод интерфейса на русский язык
|
||||||
|
|
||||||
|
### Компоненты:
|
||||||
|
- Веб-интерфейс для управления серверами и мониторинга
|
||||||
|
- API для приема метрик от агентов
|
||||||
|
- Скрипт установки агента для мониторинга серверов
|
||||||
|
- Система алертов и уведомлений
|
||||||
|
- Административная панель
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "mirivlad/monitoring-system",
|
||||||
|
"description": "Система мониторинга серверов",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1",
|
||||||
|
"slim/slim": "^4.12",
|
||||||
|
"slim/psr7": "^1.6",
|
||||||
|
"slim/twig-view": "^3.3",
|
||||||
|
"twig/twig": "^3.0",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"slim/csrf": "^1.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/",
|
||||||
|
"Config\\": "config/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "php -S localhost:8080 -t public"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"process-timeout": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
// config/DatabaseConfig.php
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class DatabaseConfig
|
||||||
|
{
|
||||||
|
private static $instance = null;
|
||||||
|
private $connection;
|
||||||
|
|
||||||
|
private $host = 'localhost';
|
||||||
|
private $db_name = 'monitoring_system';
|
||||||
|
private $username = "mon_user";
|
||||||
|
private $password = 'mon_password_123';
|
||||||
|
private $charset = 'utf8mb4';
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$dsn = "mysql:host={$this->host};dbname={$this->db_name};charset={$this->charset}";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->connection = new PDO($dsn, $this->username, $this->password, $options);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new PDOException($e->getMessage(), (int)$e->getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance()
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance->connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
// Простое логирование POST запросов для отладки
|
||||||
|
|
||||||
|
ini_set("session.save_path", "/var/www/mon/sessions");
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Логируем все входящие данные
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$uri = $_SERVER['REQUEST_URI'];
|
||||||
|
$postData = file_get_contents("php://input");
|
||||||
|
|
||||||
|
$logLine = "$timestamp - $method $uri\n";
|
||||||
|
$logLine .= "POST data: $postData\n";
|
||||||
|
$logLine .= "POST params: " . json_encode($_POST, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
$logLine .= "SESSION user_id: " . ($_SESSION['user_id'] ?? 'null') . "\n";
|
||||||
|
$logLine .= "-------------------\n";
|
||||||
|
|
||||||
|
file_put_contents('/tmp/login_debug.log', $logLine, FILE_APPEND);
|
||||||
|
|
||||||
|
// Продолжаем с обычным кодом
|
||||||
|
require_once '/var/www/mon/vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
if ($method === 'POST' && $uri === '/login') {
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
echo "DEBUG: Processing login for user: $username\n";
|
||||||
|
echo "DEBUG: POST data: $postData\n";
|
||||||
|
|
||||||
|
$userModel = new User();
|
||||||
|
$user = $userModel->authenticate($username, $password);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
echo "DEBUG: Auth successful!\n";
|
||||||
|
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
|
||||||
|
header('Location: /');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
echo "DEBUG: Auth failed!\n";
|
||||||
|
header('Location: /login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Debug Mode</h1>
|
||||||
|
<p>Check /tmp/login_debug.log for details</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
<?php
|
||||||
|
// public/index.php
|
||||||
|
|
||||||
|
use App\Controllers\AgentController;
|
||||||
|
use App\Controllers\AdminController;
|
||||||
|
use App\Controllers\AlertController;
|
||||||
|
use App\Controllers\Api\MetricsController;
|
||||||
|
use App\Controllers\GroupController;
|
||||||
|
use App\Controllers\ServerController;
|
||||||
|
use App\Controllers\ServerDetailController;
|
||||||
|
use App\Controllers\DashboardController;
|
||||||
|
use App\Middlewares\AuthMiddleware;
|
||||||
|
use App\Middlewares\SessionMiddleware;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Server as ServerModel;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Csrf\Guard;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
use Slim\Views\TwigMiddleware;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Create Slim app
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Create CSRF Guard
|
||||||
|
$csrf = new Guard($app->getResponseFactory());
|
||||||
|
$csrf->setPersistentTokenMode(true);
|
||||||
|
|
||||||
|
// Create Twig view
|
||||||
|
$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]);
|
||||||
|
|
||||||
|
// Add CSRF middleware FIRST
|
||||||
|
$app->add($csrf);
|
||||||
|
|
||||||
|
// Add Twig middleware
|
||||||
|
$twigMiddleware = TwigMiddleware::create($app, $twig);
|
||||||
|
$app->add($twigMiddleware);
|
||||||
|
|
||||||
|
// Add session middleware (MUST be after TwigMiddleware)
|
||||||
|
$sessionMiddleware = new SessionMiddleware($twig);
|
||||||
|
$app->add($sessionMiddleware);
|
||||||
|
|
||||||
|
// Add a route to get CSRF tokens via AJAX
|
||||||
|
$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) {
|
||||||
|
$data = [
|
||||||
|
'name_key' => $csrf->getTokenNameKey(),
|
||||||
|
'value_key' => $csrf->getTokenValueKey(),
|
||||||
|
'name' => $csrf->getTokenName(),
|
||||||
|
'value' => $csrf->getTokenValue()
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($data));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define /test route
|
||||||
|
$app->get('/test', function (Request $request, Response $response, $args) use ($twig) {
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Тест системы',
|
||||||
|
'message' => 'Система мониторинга запущена'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'test.twig', $templateData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login routes (without auth middleware, but with CSRF)
|
||||||
|
$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) {
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Вход в систему',
|
||||||
|
'csrf' => [
|
||||||
|
'name_key' => $csrf->getTokenNameKey(),
|
||||||
|
'value_key' => $csrf->getTokenValueKey(),
|
||||||
|
'name' => $csrf->getTokenName(),
|
||||||
|
'value' => $csrf->getTokenValue()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'login.twig', $templateData);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) {
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Validate CSRF token
|
||||||
|
$nameKey = $csrf->getTokenNameKey();
|
||||||
|
$valueKey = $csrf->getTokenValueKey();
|
||||||
|
|
||||||
|
if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) {
|
||||||
|
error_log('CSRF validation failed for /login');
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $params['username'] ?? '';
|
||||||
|
$password = $params['password'] ?? '';
|
||||||
|
|
||||||
|
$userModel = new User();
|
||||||
|
$user = $userModel->authenticate($username, $password);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout route (without auth middleware)
|
||||||
|
$app->get('/logout', function (Request $request, Response $response, $args) {
|
||||||
|
session_destroy();
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dashboard route (protected with auth middleware)
|
||||||
|
$app->get('/', function (Request $request, Response $response, $args) use ($twig) {
|
||||||
|
$serverModel = new ServerModel();
|
||||||
|
|
||||||
|
// Get statistics
|
||||||
|
$stats = $serverModel->getStats();
|
||||||
|
|
||||||
|
// Get servers with latest metrics
|
||||||
|
$servers = $serverModel->getAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Дашборд мониторинга',
|
||||||
|
'stats' => $stats,
|
||||||
|
'servers' => $servers
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'dashboard.twig', $templateData);
|
||||||
|
})->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Create controllers BEFORE routes
|
||||||
|
$groupController = new GroupController($twig);
|
||||||
|
$serverController = new ServerController($twig);
|
||||||
|
$serverDetailController = new ServerDetailController($twig);
|
||||||
|
$alertController = new AlertController($twig);
|
||||||
|
$adminController = new AdminController($twig);
|
||||||
|
$metricsController = new MetricsController();
|
||||||
|
$agentController = new AgentController();
|
||||||
|
|
||||||
|
// Routes for groups (protected with auth middleware)
|
||||||
|
$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class);
|
||||||
|
$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/groups/{id}', [$groupController, 'show'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Routes for servers (protected with auth middleware)
|
||||||
|
$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class);
|
||||||
|
$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers/{id}/thresholds', [$serverDetailController, 'saveThresholds'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers/{id}/services', [$serverDetailController, 'saveServices'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Server detail route (protected with auth middleware)
|
||||||
|
$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Alerts routes (protected with auth middleware)
|
||||||
|
$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Admin routes (protected with auth middleware)
|
||||||
|
$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// API route for agents (public, no auth middleware)
|
||||||
|
$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']);
|
||||||
|
$app->get("/api/v1/agent/{id}/services", [$metricsController, 'getServices'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Agent configuration routes (protected with auth middleware)
|
||||||
|
$app->get("/agent/{id}/config", [$agentController, 'getConfig'])->add(AuthMiddleware::class);
|
||||||
|
$app->post("/agent/{id}/config", [$agentController, 'updateConfig'])->add(AuthMiddleware::class);
|
||||||
|
$app->get("/agent/{id}/status", [$agentController, 'getStatus'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// API status endpoint (public, no auth middleware)
|
||||||
|
$app->get('/api/status', function (Request $request, Response $response, $args) {
|
||||||
|
$data = [
|
||||||
|
'status' => 'ok',
|
||||||
|
'timestamp' => date('Y-m-d H:i:s'),
|
||||||
|
'version' => '1.0.0'
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($data));
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent installation script route (public, no auth middleware)
|
||||||
|
$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']);
|
||||||
|
|
||||||
|
// Run app
|
||||||
|
$app->run();
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
<?php
|
||||||
|
// public/index.php - updated dashboard route
|
||||||
|
|
||||||
|
use App\Controllers\AgentController;
|
||||||
|
use App\Controllers\AdminController;
|
||||||
|
use App\Controllers\AlertController;
|
||||||
|
use App\Controllers\Api\MetricsController;
|
||||||
|
use App\Controllers\GroupController;
|
||||||
|
use App\Controllers\ServerController;
|
||||||
|
use App\Controllers\ServerDetailController;
|
||||||
|
use App\Controllers\DashboardController;
|
||||||
|
use App\Middlewares\AuthMiddleware;
|
||||||
|
use App\Middlewares\SessionMiddleware;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Server as ServerModel;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Csrf\Guard;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
use Slim\Views\TwigMiddleware;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Create Slim app
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Create CSRF Guard
|
||||||
|
$csrf = new Guard($app->getResponseFactory());
|
||||||
|
$csrf->setPersistentTokenMode(true);
|
||||||
|
|
||||||
|
// Create Twig view
|
||||||
|
$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]);
|
||||||
|
|
||||||
|
// Add CSRF middleware FIRST
|
||||||
|
$app->add($csrf);
|
||||||
|
|
||||||
|
// Add Twig middleware
|
||||||
|
$twigMiddleware = TwigMiddleware::create($app, $twig);
|
||||||
|
$app->add($twigMiddleware);
|
||||||
|
$sessionMiddleware = new AppMiddlewaresSessionMiddleware($twig);
|
||||||
|
$app->add($sessionMiddleware);
|
||||||
|
// Add session data to Twig$sessionMiddleware = new SessionMiddleware($twig);$app->add($sessionMiddleware);
|
||||||
|
|
||||||
|
// Add a route to get CSRF tokens via AJAX
|
||||||
|
$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) {
|
||||||
|
$data = [
|
||||||
|
'name_key' => $csrf->getTokenNameKey(),
|
||||||
|
'value_key' => $csrf->getTokenValueKey(),
|
||||||
|
'name' => $csrf->getTokenName(),
|
||||||
|
'value' => $csrf->getTokenValue()
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($data));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define /test route
|
||||||
|
$app->get('/test', function (Request $request, Response $response, $args) use ($twig) {
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Тест системы',
|
||||||
|
'message' => 'Система мониторинга запущена'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'test.twig', $templateData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login routes (without auth middleware, but with CSRF)
|
||||||
|
$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) {
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Вход в систему',
|
||||||
|
'csrf' => [
|
||||||
|
'name_key' => $csrf->getTokenNameKey(),
|
||||||
|
'value_key' => $csrf->getTokenValueKey(),
|
||||||
|
'name' => $csrf->getTokenName(),
|
||||||
|
'value' => $csrf->getTokenValue()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'login.twig', $templateData);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) {
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Validate CSRF token
|
||||||
|
$nameKey = $csrf->getTokenNameKey();
|
||||||
|
$valueKey = $csrf->getTokenValueKey();
|
||||||
|
|
||||||
|
if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) {
|
||||||
|
error_log('CSRF validation failed for /login');
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $params['username'] ?? '';
|
||||||
|
$password = $params['password'] ?? '';
|
||||||
|
|
||||||
|
$userModel = new User();
|
||||||
|
$user = $userModel->authenticate($username, $password);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout route (without auth middleware)
|
||||||
|
$app->get('/logout', function (Request $request, Response $response, $args) {
|
||||||
|
session_destroy();
|
||||||
|
return $response->withHeader('Location', '/login')->withStatus(302);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dashboard route (protected with auth middleware)
|
||||||
|
$app->get('/', function (Request $request, Response $response, $args) use ($twig) {
|
||||||
|
$serverModel = new ServerModel();
|
||||||
|
|
||||||
|
// Get statistics
|
||||||
|
$stats = $serverModel->getStats();
|
||||||
|
|
||||||
|
// Get servers with latest metrics
|
||||||
|
$servers = $serverModel->getAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Дашборд мониторинга',
|
||||||
|
'stats' => $stats,
|
||||||
|
'servers' => $servers
|
||||||
|
];
|
||||||
|
|
||||||
|
return $twig->render($response, 'dashboard.twig', $templateData);
|
||||||
|
})->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Routes for groups (protected with auth middleware)
|
||||||
|
$groupController = new GroupController($twig);
|
||||||
|
|
||||||
|
$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class);
|
||||||
|
$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Routes for servers (protected with auth middleware)
|
||||||
|
$serverController = new ServerController($twig);
|
||||||
|
|
||||||
|
$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class);
|
||||||
|
$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class);
|
||||||
|
$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Server detail route (protected with auth middleware)
|
||||||
|
$serverDetailController = new ServerDetailController($twig);
|
||||||
|
|
||||||
|
$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Alerts routes (protected with auth middleware)
|
||||||
|
$alertController = new AlertController($twig);
|
||||||
|
|
||||||
|
$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// Admin routes (protected with auth middleware)
|
||||||
|
$adminController = new AdminController($twig);
|
||||||
|
|
||||||
|
$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class);
|
||||||
|
$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
// API route for agents (public, no auth middleware)
|
||||||
|
$metricsController = new MetricsController();
|
||||||
|
|
||||||
|
$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']);
|
||||||
|
|
||||||
|
// API status endpoint (public, no auth middleware)
|
||||||
|
$app->get('/api/status', function (Request $request, Response $response, $args) {
|
||||||
|
$data = [
|
||||||
|
'status' => 'ok',
|
||||||
|
'timestamp' => date('Y-m-d H:i:s'),
|
||||||
|
'version' => '1.0.0'
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($data));
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent installation script route (public, no auth middleware)
|
||||||
|
$agentController = new AgentController();
|
||||||
|
|
||||||
|
$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']);
|
||||||
|
|
||||||
|
// Run app
|
||||||
|
$app->run();
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Простая проверка пароля (для теста)
|
||||||
|
if (isset($_POST['username']) && isset($_POST['password'])) {
|
||||||
|
$username = $_POST['username'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
// Хешированный пароль для admin_test_2026
|
||||||
|
$correctHash = '$2y$10$5PhDSHiF1J6yxcEldOsluOSmUYaO1bWa7swFmfmP/Slj.HJOh5t2O';
|
||||||
|
$inputHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Для теста используем прямое сравнение хешей
|
||||||
|
if ($username === 'admin' && password_verify($password, $correctHash)) {
|
||||||
|
$_SESSION['user_id'] = 1;
|
||||||
|
$_SESSION['username'] = 'admin';
|
||||||
|
$_SESSION['role'] = 'admin';
|
||||||
|
$_SESSION['logged_in'] = time();
|
||||||
|
|
||||||
|
header('Location: /');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Тест входа</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Тестовый вход</h1>
|
||||||
|
<form method="post">
|
||||||
|
<p>
|
||||||
|
Логин: <input type="text" name="username" value="admin">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Пароль: <input type="password" name="password">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button type="submit">Войти</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p>Тестовые креды: admin / admin_test_2026</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'session_id' => session_id(),
|
||||||
|
'user_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'username' => $_SESSION['username'] ?? null,
|
||||||
|
'role' => $_SESSION['role'] ?? null,
|
||||||
|
'session_data' => $_SESSION
|
||||||
|
], JSON_PRETTY_PRINT);
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Проверяем существующую сессию
|
||||||
|
echo "Текущая сессия:\n";
|
||||||
|
echo "Session ID: " . session_id() . "\n";
|
||||||
|
echo "Session data: " . json_encode($_SESSION, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
|
||||||
|
// Если сессия пуста, создаем пользователя admin
|
||||||
|
if (!isset($_SESSION["user_id"])) {
|
||||||
|
$_SESSION["user_id"] = 1;
|
||||||
|
$_SESSION["username"] = "admin";
|
||||||
|
$_SESSION["role"] = "admin";
|
||||||
|
$_SESSION["created_at"] = time();
|
||||||
|
|
||||||
|
echo "\n✅ Сессия создана!\n";
|
||||||
|
} else {
|
||||||
|
echo "\n✅ Сессия уже существует!\n";
|
||||||
|
echo "Время создания: " . date("Y-m-d H:i:s", $_SESSION["created_at"]) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Информация о cookies
|
||||||
|
echo "\n🍪 Cookie информация:\n
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once '/var/www/mon/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
$pdo = DatabaseConfig::getInstance();
|
||||||
|
$userModel = new User();
|
||||||
|
|
||||||
|
// Устанавливаем пароль admin в сессии напрямую
|
||||||
|
$_SESSION['user_id'] = 1;
|
||||||
|
$_SESSION['username'] = 'admin';
|
||||||
|
$_SESSION['role'] = 'admin';
|
||||||
|
|
||||||
|
echo "✅ Сессия admin создана!\n";
|
||||||
|
echo "Session ID: " . session_id() . "\n";
|
||||||
|
echo "Session data: " . json_encode($_SESSION, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
-- Структура базы данных для системы мониторинга
|
||||||
|
|
||||||
|
-- Таблица пользователей
|
||||||
|
CREATE DATABASE IF NOT EXISTS monitoring_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE monitoring_system;
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(100),
|
||||||
|
role ENUM('admin', 'user') DEFAULT 'user',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица настроек уведомлений пользователей
|
||||||
|
CREATE TABLE user_notification_settings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
telegram_chat_id VARCHAR(50),
|
||||||
|
email_for_alerts VARCHAR(100),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица групп серверов
|
||||||
|
CREATE TABLE server_groups (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
icon VARCHAR(50),
|
||||||
|
color VARCHAR(20)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица серверов
|
||||||
|
CREATE TABLE servers (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
address VARCHAR(255),
|
||||||
|
group_id INT,
|
||||||
|
description TEXT,
|
||||||
|
last_metrics_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (group_id) REFERENCES server_groups(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица названий метрик
|
||||||
|
CREATE TABLE metric_names (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
unit VARCHAR(20),
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Вставляем стандартные метрики
|
||||||
|
INSERT INTO metric_names (name, unit, description) VALUES
|
||||||
|
('cpu_load', '%', 'Загрузка процессора'),
|
||||||
|
('ram_used', '%', 'Использование оперативной памяти'),
|
||||||
|
('disk_used', '%', 'Использование диска'),
|
||||||
|
('network_in', 'MB/s', 'Скорость приема сети'),
|
||||||
|
('network_out', 'MB/s', 'Скорость передачи сети');
|
||||||
|
|
||||||
|
-- Таблица пороговых значений метрик
|
||||||
|
CREATE TABLE metric_thresholds (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
server_id INT NOT NULL,
|
||||||
|
metric_name_id INT NOT NULL,
|
||||||
|
warning_threshold DECIMAL(8,2),
|
||||||
|
critical_threshold DECIMAL(8,2),
|
||||||
|
duration INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (metric_name_id) REFERENCES metric_names(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица метрик серверов
|
||||||
|
CREATE TABLE server_metrics (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
server_id INT NOT NULL,
|
||||||
|
metric_name_id INT NOT NULL,
|
||||||
|
value DECIMAL(8,2),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_server_metric_time (server_id, metric_name_id, created_at),
|
||||||
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (metric_name_id) REFERENCES metric_names(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица токенов агентов
|
||||||
|
CREATE TABLE agent_tokens (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
server_id INT UNIQUE NOT NULL,
|
||||||
|
token_hash VARCHAR(64) NOT NULL, -- SHA-256 hash
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Таблица алертов
|
||||||
|
CREATE TABLE alerts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
server_id INT NOT NULL,
|
||||||
|
metric_name VARCHAR(50) NOT NULL,
|
||||||
|
value DECIMAL(8,2),
|
||||||
|
severity ENUM('warning', 'critical') NOT NULL,
|
||||||
|
resolved BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
resolved_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/AdminController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class AdminController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
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->execute();
|
||||||
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Управление пользователями',
|
||||||
|
'users' => $users
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'admin/users.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notificationSettings(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
// Только для администраторов
|
||||||
|
if ($_SESSION['role'] !== 'admin') {
|
||||||
|
return $response->withHeader('Location', '/')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Настройки уведомлений'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'admin/notifications.twig', $templateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,424 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/AgentController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use App\Utils\EncryptionHelper;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class AgentController extends Model
|
||||||
|
{
|
||||||
|
public function generateInstallScript(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
$token = $queryParams['token'] ?? null;
|
||||||
|
$server_id = $queryParams['server_id'] ?? null;
|
||||||
|
|
||||||
|
// Если передан server_id, получаем оригинальный токен из зашифрованного
|
||||||
|
if (!empty($server_id) && empty($token)) {
|
||||||
|
$stmt = $this->pdo->prepare("SELECT encrypted_token FROM agent_tokens WHERE server_id = :server_id LIMIT 1");
|
||||||
|
$stmt->execute([':server_id' => $server_id]);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($result && !empty($result['encrypted_token'])) {
|
||||||
|
$token = EncryptionHelper::decrypt($result['encrypted_token']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($token)) {
|
||||||
|
$response->getBody()->write('Token is required');
|
||||||
|
return $response->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiUrl = 'https://mon.mirv.top/api/v1/metrics';
|
||||||
|
|
||||||
|
// Формируем скрипт с прямой подстановкой значений
|
||||||
|
$script = "#!/bin/bash
|
||||||
|
|
||||||
|
# Скрипт установки агента мониторинга с поддержкой сервисов
|
||||||
|
# Сгенерировано автоматически
|
||||||
|
|
||||||
|
TOKEN='" . $token . "'
|
||||||
|
API_URL='" . $apiUrl . "'
|
||||||
|
|
||||||
|
echo 'Установка агента мониторинга...'
|
||||||
|
|
||||||
|
# Проверяем наличие Python3
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo 'Установка Python3...'
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y python3 python3-pip
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Устанавливаем psutil
|
||||||
|
pip3 install psutil || easy_install3 psutil
|
||||||
|
|
||||||
|
# Создаем директорию для агента
|
||||||
|
mkdir -p /opt/server-monitor-agent
|
||||||
|
cd /opt/server-monitor-agent
|
||||||
|
|
||||||
|
# Создаем конфигурационный файл
|
||||||
|
echo '{
|
||||||
|
\\\"token\\\": \\\"" . $token . "\\\"\\,
|
||||||
|
\\\"api_url\\\": \\\"" . $apiUrl . "\\\"\\,
|
||||||
|
\\\"interval_seconds\\\": 60
|
||||||
|
}' > config.json
|
||||||
|
|
||||||
|
# Создаем Python-скрипт агента с поддержкой сервисов
|
||||||
|
cat > agent.py << 'PYTHON_EOF'
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import psutil
|
||||||
|
import requests
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def get_metrics():
|
||||||
|
\\\"\\\"\\\"Сбор системных метрик\\\"\\\"\\\"
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
disk_usage = psutil.disk_usage('/')
|
||||||
|
|
||||||
|
# Получаем сетевую статистику
|
||||||
|
try:
|
||||||
|
net_io = psutil.net_io_counters()
|
||||||
|
except:
|
||||||
|
net_io = None
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
'cpu_load': round(cpu_percent, 2),
|
||||||
|
'ram_used': round(memory.percent, 2),
|
||||||
|
'disk_used': round((disk_usage.used / disk_usage.total) * 100, 2),
|
||||||
|
'network_in': round((net_io.bytes_recv / (1024*1024)) if net_io else 0, 2), # MB
|
||||||
|
'network_out': round((net_io.bytes_sent / (1024*1024)) if net_io else 0, 2) # MB
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
def get_services():
|
||||||
|
\\\"\\\"\\\"Сбор статусов всех сервисов\\\"\\\"\\\"
|
||||||
|
services = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем список всех сервисов
|
||||||
|
result = subprocess.run(
|
||||||
|
['systemctl', 'list-units', '--type=service', '--all', '--no-pager'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = result.stdout.strip().split('\\n')
|
||||||
|
|
||||||
|
for line in lines[1:]: # Пропускаем заголовок
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
service_name = parts[0].replace('.service', '')
|
||||||
|
load_state = parts[1]
|
||||||
|
active_state = parts[2]
|
||||||
|
sub_state = parts[3] if len(parts) > 3 else ''
|
||||||
|
|
||||||
|
# Определяем статус сервиса
|
||||||
|
if active_state == 'active' and sub_state == 'running':
|
||||||
|
status = 'running'
|
||||||
|
elif active_state in ['inactive', 'failed', 'dead']:
|
||||||
|
status = 'stopped'
|
||||||
|
else:
|
||||||
|
status = 'unknown'
|
||||||
|
|
||||||
|
# Пропускаем системные сервисы без .service в имени
|
||||||
|
if not service_name.startswith('system-'):
|
||||||
|
services.append({
|
||||||
|
'name': service_name,
|
||||||
|
'status': status,
|
||||||
|
'load_state': load_state,
|
||||||
|
'active_state': active_state,
|
||||||
|
'sub_state': sub_state
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Ошибка при получении списка сервисов: {e}')
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
||||||
|
def get_config_from_server():
|
||||||
|
\\\"\\\"\\\"Получение конфигурации с сервера\\\"\\\"\\\"
|
||||||
|
try:
|
||||||
|
with open('config.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Ошибка чтения конфига: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
token = config.get('token')
|
||||||
|
if not token:
|
||||||
|
print('Отсутствует токен в конфиге')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Определяем URL для получения конфигурации
|
||||||
|
server_id = token.split('-')[0] if '-' in token else '1'
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f\\\"\\\"{config['api_url']}/agent/{server_id}/config\\\"\\\"\\\",
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
server_config = response.json()
|
||||||
|
|
||||||
|
# Обновляем локальный конфиг
|
||||||
|
config['interval_seconds'] = server_config.get('interval_seconds', config['interval_seconds'])
|
||||||
|
config['monitor_services'] = server_config.get('monitor_services', config.get('monitor_services', []))
|
||||||
|
|
||||||
|
# Сохраняем обновленный конфиг
|
||||||
|
with open('config.json', 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
|
return config
|
||||||
|
else:
|
||||||
|
print(f'Ошибка получения конфига с сервера: {response.status_code}')
|
||||||
|
return config
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Ошибка подключения к серверу: {e}')
|
||||||
|
return config
|
||||||
|
|
||||||
|
def send_metrics(config, metrics, services):
|
||||||
|
\\\"\\\"\\\"Отправка метрик и сервисов на сервер\\\"\\\"\\\"
|
||||||
|
data = {
|
||||||
|
'token': config['token'],
|
||||||
|
'metrics': metrics,
|
||||||
|
'services': services
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
config['api_url'],
|
||||||
|
json=data,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f'{datetime.now().strftime(\\\"%Y-%m-%d %H:%M:%S\\\")} - Метрики отправлены успешно')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f'Ошибка отправки метрик: {response.status_code}')
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Ошибка отправки метрик: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
\\\"\\\"\\\"Главная функция агента\\\"\\\"\\\"
|
||||||
|
print('Агент мониторинга запущен...')
|
||||||
|
|
||||||
|
# Загружаем конфигурацию
|
||||||
|
config = get_config_from_server()
|
||||||
|
if not config:
|
||||||
|
print('Не удалось загрузить конфигурацию')
|
||||||
|
return
|
||||||
|
|
||||||
|
interval = config.get('interval_seconds', 60)
|
||||||
|
monitor_services = config.get('monitor_services', [])
|
||||||
|
|
||||||
|
print(f'Интервал отправки: {interval} сек')
|
||||||
|
print(f'Мониторинг сервисов: {\\\"включен\\\" if monitor_services else \\\"все сервисы\\\"}')
|
||||||
|
|
||||||
|
last_config_update = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Проверяем нужно ли обновить конфиг (каждые 5 минут)
|
||||||
|
if time.time() - last_config_update > 300:
|
||||||
|
print('Проверка обновления конфигурации...')
|
||||||
|
config = get_config_from_server()
|
||||||
|
last_config_update = time.time()
|
||||||
|
|
||||||
|
# Обновляем интервал если изменился
|
||||||
|
interval = config.get('interval_seconds', 60)
|
||||||
|
monitor_services = config.get('monitor_services', [])
|
||||||
|
|
||||||
|
# Собираем метрики
|
||||||
|
metrics = get_metrics()
|
||||||
|
|
||||||
|
# Собираем сервисы
|
||||||
|
services = get_services()
|
||||||
|
|
||||||
|
# Если указаны конкретные сервисы для мониторинга - фильтруем
|
||||||
|
if monitor_services:
|
||||||
|
services = [s for s in services if s['name'] in monitor_services]
|
||||||
|
print(f'Мониторинг {len(services)} сервисов: {[s[\\\"name\\\"] for s in services]}')
|
||||||
|
|
||||||
|
# Отправляем данные
|
||||||
|
success = send_metrics(config, metrics, services)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f'Метрики отправлены: CPU={metrics[\\\"cpu_load\\\"]}%, RAM={metrics[\\\"ram_used\\\"]}%, Disk={metrics[\\\"disk_used\\\"]}%')
|
||||||
|
else:
|
||||||
|
print('Ошибка отправки метрик')
|
||||||
|
|
||||||
|
# Ждем указанный интервал
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('Агент остановлен')
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Ошибка: {e}')
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
PYTHON_EOF
|
||||||
|
|
||||||
|
# Создаем systemd сервис
|
||||||
|
cat > /etc/systemd/system/server-monitor-agent.service << 'SERVICE_EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Server Monitor Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/server-monitor-agent
|
||||||
|
ExecStart=/usr/bin/python3 /opt/server-monitor-agent/agent.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE_EOF
|
||||||
|
|
||||||
|
# Делаем скрипт исполняемым
|
||||||
|
chmod +x agent.py
|
||||||
|
|
||||||
|
# Перезагружаем systemd
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Включаем автозапуск сервиса
|
||||||
|
systemctl enable server-monitor-agent
|
||||||
|
|
||||||
|
# Запускаем сервис
|
||||||
|
systemctl start server-monitor-agent
|
||||||
|
|
||||||
|
echo 'Агент мониторинга установлен и запущен!'
|
||||||
|
echo 'Статус сервиса:'
|
||||||
|
systemctl status server-monitor-agent
|
||||||
|
";
|
||||||
|
|
||||||
|
$response->getBody()->write($script);
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/x-shellscript')
|
||||||
|
->withHeader('Content-Disposition', 'attachment; filename="install.sh"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$serverId = $args['id'];
|
||||||
|
|
||||||
|
// Получаем конфигурацию агента
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT interval_seconds, monitor_services, enabled
|
||||||
|
FROM agent_configs
|
||||||
|
WHERE server_id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
$config = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$config) {
|
||||||
|
// Если конфигурации нет - создаем с дефолтными значениями
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
||||||
|
VALUES (:server_id, 60, '[]', TRUE)
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'interval_seconds' => 60,
|
||||||
|
'monitor_services' => [],
|
||||||
|
'enabled' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($config));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateConfig(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$serverId = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Получаем и десериализуем массив сервисов
|
||||||
|
$monitorServices = $params['monitor_services'] ?? [];
|
||||||
|
|
||||||
|
if (is_string($monitorServices)) {
|
||||||
|
$monitorServices = json_decode($monitorServices, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация интервала
|
||||||
|
$interval = max(10, min(3600, (int)($params['interval_seconds'] ?? 60)));
|
||||||
|
|
||||||
|
// Обновляем конфигурацию
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
||||||
|
VALUES (:server_id, :interval, :services, TRUE)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
interval_seconds = VALUES(interval_seconds),
|
||||||
|
monitor_services = VALUES(monitor_services),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':interval' => $interval,
|
||||||
|
':services' => json_encode($monitorServices)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Обновляем статус проверки сервисов на сервере
|
||||||
|
$enabled = $params['enabled'] ?? true;
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE servers SET service_check_enabled = :enabled WHERE id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':enabled' => $enabled ? 1 : 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$serverId = $args['id'];
|
||||||
|
|
||||||
|
// Получаем последний раз когда агент был активен
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT s.last_metrics_at, s.last_service_check_at, ac.enabled
|
||||||
|
FROM servers s
|
||||||
|
LEFT JOIN agent_configs ac ON s.id = ac.server_id
|
||||||
|
WHERE s.id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
$result = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'Server not found']));
|
||||||
|
return $response->withStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'status' => $result['enabled'] ? 'active' : 'disabled',
|
||||||
|
'last_seen' => $result['last_metrics_at'],
|
||||||
|
'last_service_check' => $result['last_service_check_at']
|
||||||
|
];
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($data));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/AlertController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class AlertController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT a.*, s.name as server_name
|
||||||
|
FROM alerts a
|
||||||
|
JOIN servers s ON a.server_id = s.id
|
||||||
|
WHERE a.resolved = 0
|
||||||
|
ORDER BY a.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$alerts = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Алерты',
|
||||||
|
'alerts' => $alerts
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'alerts/index.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsResolved(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE alerts
|
||||||
|
SET resolved = 1, resolved_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([':id' => $id]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/alerts')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/alerts')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/Api/MetricsController.php
|
||||||
|
|
||||||
|
namespace App\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
|
||||||
|
class MetricsController extends Model
|
||||||
|
{
|
||||||
|
public function collectMetrics(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$input = json_decode($request->getBody(), true);
|
||||||
|
|
||||||
|
if (!$input || !isset($input['token'])) {
|
||||||
|
return $response->withStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $input['token'];
|
||||||
|
$metrics = $input['metrics'] ?? [];
|
||||||
|
$services = $input['services'] ?? [];
|
||||||
|
|
||||||
|
// Проверяем токен (сравниваем хеш)
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT at.server_id, s.name as server_name
|
||||||
|
FROM agent_tokens at
|
||||||
|
JOIN servers s ON at.server_id = s.id
|
||||||
|
WHERE at.token_hash = :token_hash
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([':token_hash' => $tokenHash]);
|
||||||
|
$tokenInfo = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$tokenInfo) {
|
||||||
|
return $response->withStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverId = $tokenInfo['server_id'];
|
||||||
|
|
||||||
|
// Обновляем время последних метрик для сервера
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE servers
|
||||||
|
SET last_metrics_at = NOW()
|
||||||
|
WHERE id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
|
||||||
|
// Сохраняем метрики если есть
|
||||||
|
if (!empty($metrics)) {
|
||||||
|
foreach ($metrics as $metricName => $value) {
|
||||||
|
// Получаем ID метрики из справочника
|
||||||
|
$stmt = $this->pdo->prepare("SELECT id FROM metric_names WHERE name = :name");
|
||||||
|
$stmt->execute([':name' => $metricName]);
|
||||||
|
$metricInfo = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($metricInfo) {
|
||||||
|
$metricId = $metricInfo['id'];
|
||||||
|
|
||||||
|
// Сохраняем метрику
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO server_metrics (server_id, metric_name_id, value)
|
||||||
|
VALUES (:server_id, :metric_name_id, :value)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':metric_name_id' => $metricId,
|
||||||
|
':value' => $value
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Проверяем пороги
|
||||||
|
$this->checkThresholds($serverId, $metricId, $value, $metricName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем статусы сервисов если есть
|
||||||
|
if (!empty($services)) {
|
||||||
|
foreach ($services as $service) {
|
||||||
|
$serviceName = $service['name'] ?? '';
|
||||||
|
$serviceStatus = $service['status'] ?? 'unknown';
|
||||||
|
$loadState = $service['load_state'] ?? '';
|
||||||
|
$activeState = $service['active_state'] ?? '';
|
||||||
|
$subState = $service['sub_state'] ?? '';
|
||||||
|
|
||||||
|
if (empty($serviceName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статус сервиса (INSERT OR UPDATE)
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO service_status (server_id, service_name, status, load_state, active_state, sub_state, updated_at)
|
||||||
|
VALUES (:server_id, :service_name, :status, :load_state, :active_state, :sub_state, NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
status = VALUES(status),
|
||||||
|
load_state = VALUES(load_state),
|
||||||
|
active_state = VALUES(active_state),
|
||||||
|
sub_state = VALUES(sub_state),
|
||||||
|
updated_at = NOW()
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':service_name' => $serviceName,
|
||||||
|
':status' => $serviceStatus,
|
||||||
|
':load_state' => $loadState,
|
||||||
|
':active_state' => $activeState,
|
||||||
|
':sub_state' => $subState
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Если сервис остановлен и включено его мониторинг - создаем алерт
|
||||||
|
if ($serviceStatus === 'stopped') {
|
||||||
|
$this->createServiceAlert($serverId, $serviceName, $serviceStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем время последней проверки сервисов
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE servers
|
||||||
|
SET last_service_check_at = NOW()
|
||||||
|
WHERE id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем время последнего использования токена
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE agent_tokens
|
||||||
|
SET last_used_at = NOW()
|
||||||
|
WHERE server_id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
|
||||||
|
return $response->withStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
WHERE server_id = :server_id
|
||||||
|
ORDER BY service_name
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
$services = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode(['services' => $services]));
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/DashboardController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
|
class DashboardController
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
private $serverModel;
|
||||||
|
|
||||||
|
public function __construct($twig)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
$this->serverModel = new Server();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
// Получаем статистику
|
||||||
|
$stats = $this->serverModel->getStats();
|
||||||
|
|
||||||
|
// Получаем список серверов с последними метриками
|
||||||
|
$servers = $this->serverModel->getAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Дашборд мониторинга',
|
||||||
|
'stats' => $stats,
|
||||||
|
'servers' => $servers
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/GroupController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class GroupController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name");
|
||||||
|
$stmt->execute();
|
||||||
|
$groups = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Группы серверов',
|
||||||
|
'groups' => $groups
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'groups/index.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Создать группу'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'groups/create.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO server_groups (name, description, icon, color)
|
||||||
|
VALUES (:name, :description, :icon, :color)
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':name' => $params['name'],
|
||||||
|
':description' => $params['description'] ?? '',
|
||||||
|
':icon' => $params['icon'] ?? '',
|
||||||
|
':color' => $params['color'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/groups/create')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM server_groups WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$group = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$group) {
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Редактировать группу',
|
||||||
|
'group' => $group
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'groups/edit.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE server_groups
|
||||||
|
SET name = :name, description = :description, icon = :icon, color = :color
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':id' => $id,
|
||||||
|
':name' => $params['name'],
|
||||||
|
':description' => $params['description'] ?? '',
|
||||||
|
':icon' => $params['icon'] ?? '',
|
||||||
|
':color' => $params['color'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/groups/' . $id . '/edit')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM server_groups WHERE id = :id");
|
||||||
|
$result = $stmt->execute([':id' => $id]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
// Получаем информацию о группе
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM server_groups WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$group = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$group) {
|
||||||
|
return $response->withHeader('Location', '/groups')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем серверы в этой группе
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT s.*,
|
||||||
|
(SELECT created_at FROM server_metrics sm WHERE sm.server_id = s.id ORDER BY sm.created_at DESC LIMIT 1) as last_metrics_at
|
||||||
|
FROM servers s
|
||||||
|
WHERE s.group_id = :group_id
|
||||||
|
ORDER BY s.name
|
||||||
|
");
|
||||||
|
$stmt->execute([':group_id' => $id]);
|
||||||
|
$servers = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Получаем статистику для каждого сервера
|
||||||
|
$serverStats = [];
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
$stmt = $this->pdo->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
|
||||||
|
ORDER BY sm.created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $server['id']]);
|
||||||
|
|
||||||
|
$metrics = [];
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
if (!isset($metrics[$row['name']])) {
|
||||||
|
$metrics[$row['name']] = [
|
||||||
|
'value' => $row['value'],
|
||||||
|
'unit' => $row['unit']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverStats[$server['id']] = $metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Группа: ' . $group['name'],
|
||||||
|
'group' => $group,
|
||||||
|
'servers' => $servers,
|
||||||
|
'serverStats' => $serverStats
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'groups/show.twig', $templateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/ServerController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use App\Utils\EncryptionHelper;
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class ServerController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT s.*, sg.name as group_name
|
||||||
|
FROM servers s
|
||||||
|
LEFT JOIN server_groups sg ON s.group_id = sg.id
|
||||||
|
ORDER BY s.name
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$servers = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Серверы',
|
||||||
|
'servers' => $servers
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/index.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name");
|
||||||
|
$stmt->execute();
|
||||||
|
$groups = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Добавить сервер',
|
||||||
|
'groups' => $groups
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/create.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Генерируем уникальный токен
|
||||||
|
$token = bin2hex(random_bytes(16)); // 32-символьный токен
|
||||||
|
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сохраняем сервер
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO servers (name, address, group_id, description)
|
||||||
|
VALUES (:name, :address, :group_id, :description)
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':name' => $params['name'],
|
||||||
|
':address' => $params['address'] ?? '',
|
||||||
|
':group_id' => $params['group_id'] ?? null,
|
||||||
|
':description' => $params['description'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serverId = $this->pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Сохраняем хеш токена и зашифрованный токен
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
|
$encryptedToken = EncryptionHelper::encrypt($token);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_tokens (server_id, token_hash, encrypted_token)
|
||||||
|
VALUES (:server_id, :token_hash, :encrypted_token)
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':token_hash' => $tokenHash,
|
||||||
|
':encrypted_token' => $encryptedToken
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->pdo->commit();
|
||||||
|
|
||||||
|
// Передаем токен для отображения на странице
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Сервер добавлен',
|
||||||
|
'server' => [
|
||||||
|
'id' => $serverId,
|
||||||
|
'name' => $params['name']
|
||||||
|
],
|
||||||
|
'token' => $token
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/created.twig', $templateData);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->pdo->rollback();
|
||||||
|
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/servers/create')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM servers WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name");
|
||||||
|
$stmt->execute();
|
||||||
|
$groups = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Редактировать сервер',
|
||||||
|
'server' => $server,
|
||||||
|
'groups' => $groups
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/edit.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE servers
|
||||||
|
SET name = :name, address = :address, group_id = :group_id, description = :description
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':id' => $id,
|
||||||
|
':name' => $params['name'],
|
||||||
|
':address' => $params['address'] ?? '',
|
||||||
|
':group_id' => $params['group_id'] ?? null,
|
||||||
|
':description' => $params['description'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM servers WHERE id = :id");
|
||||||
|
$result = $stmt->execute([':id' => $id]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateToken(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
// Генерируем новый токен
|
||||||
|
$newToken = bin2hex(random_bytes(16));
|
||||||
|
$tokenHash = hash('sha256', $newToken);
|
||||||
|
$encryptedToken = EncryptionHelper::encrypt($newToken);
|
||||||
|
|
||||||
|
// Обновляем или создаем запись в agent_tokens
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_tokens (server_id, token_hash, encrypted_token)
|
||||||
|
VALUES (:server_id, :token_hash, :encrypted_token)
|
||||||
|
ON DUPLICATE KEY UPDATE token_hash = :token_hash, encrypted_token = :encrypted_token
|
||||||
|
");
|
||||||
|
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':server_id' => $id,
|
||||||
|
':token_hash' => $tokenHash,
|
||||||
|
':encrypted_token' => $encryptedToken
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
// Перенаправляем обратно на страницу редактирования
|
||||||
|
return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302);
|
||||||
|
} else {
|
||||||
|
// TODO: Обработка ошибки
|
||||||
|
return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/ServerDetailController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class ServerDetailController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
// Получаем информацию о сервере
|
||||||
|
$stmt = $this->pdo->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 = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем период для выборки метрик
|
||||||
|
$period = $request->getQueryParams()['period'] ?? '24h';
|
||||||
|
|
||||||
|
// Определяем интервал времени в зависимости от периода
|
||||||
|
$interval = match($period) {
|
||||||
|
'7d' => 'INTERVAL 7 DAY',
|
||||||
|
'30d' => 'INTERVAL 30 DAY',
|
||||||
|
default => 'INTERVAL 24 HOUR'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем последние метрики для этого сервера
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT sm.value, mn.name, mn.unit, sm.created_at
|
||||||
|
FROM server_metrics sm
|
||||||
|
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
||||||
|
WHERE sm.server_id = :id
|
||||||
|
AND sm.created_at >= DATE_SUB(NOW(), {$interval})
|
||||||
|
ORDER BY sm.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$metrics = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Группируем метрики по типу (CPU, RAM, Disk)
|
||||||
|
$groupedMetrics = [];
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
$metricName = $metric['name'];
|
||||||
|
if (!isset($groupedMetrics[$metricName])) {
|
||||||
|
$groupedMetrics[$metricName] = [];
|
||||||
|
}
|
||||||
|
$groupedMetrics[$metricName][] = $metric;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущие пороговые значения для сервера
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
|
||||||
|
FROM metric_thresholds mt
|
||||||
|
JOIN metric_names mn ON mt.metric_name_id = mn.id
|
||||||
|
WHERE mt.server_id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$existingThresholds = [];
|
||||||
|
foreach ($stmt->fetchAll() as $threshold) {
|
||||||
|
$existingThresholds[$threshold['name']] = [
|
||||||
|
'warning' => $threshold['warning_threshold'],
|
||||||
|
'critical' => $threshold['critical_threshold'],
|
||||||
|
'duration' => $threshold['duration']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем все типы метрик
|
||||||
|
$stmt = $this->pdo->query("SELECT id, name, unit FROM metric_names ORDER BY name");
|
||||||
|
$allMetricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Получаем список сервисов
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT service_name, status, load_state, active_state, sub_state
|
||||||
|
FROM service_status
|
||||||
|
WHERE server_id = :server_id
|
||||||
|
ORDER BY service_name
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
$allServices = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Получаем список сервисов для мониторинга из конфигурации агента
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT monitor_services FROM agent_configs WHERE server_id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
$agentConfig = $stmt->fetch();
|
||||||
|
|
||||||
|
$monitorServices = [];
|
||||||
|
if ($agentConfig && !empty($agentConfig['monitor_services'])) {
|
||||||
|
$monitorServices = json_decode($agentConfig['monitor_services'], true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Сервер: ' . $server['name'],
|
||||||
|
'server' => $server,
|
||||||
|
'metrics' => $groupedMetrics,
|
||||||
|
'allMetricTypes' => $allMetricTypes,
|
||||||
|
'existingThresholds' => $existingThresholds,
|
||||||
|
'allServices' => $allServices,
|
||||||
|
'monitorServices' => $monitorServices,
|
||||||
|
'period' => $period,
|
||||||
|
'request' => $request->getQueryParams()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveThresholds(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Получаем все типы метрик
|
||||||
|
$stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name");
|
||||||
|
$metricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Удаляем старые пороги для этого сервера
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
|
||||||
|
// Добавляем новые пороги
|
||||||
|
$insertStmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration)
|
||||||
|
VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration)
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($metricTypes as $metricType) {
|
||||||
|
$warning = $params[$metricType['name'] . '_warning'] ?? '';
|
||||||
|
$critical = $params[$metricType['name'] . '_critical'] ?? '';
|
||||||
|
$duration = $params[$metricType['name'] . '_duration'] ?? 0;
|
||||||
|
|
||||||
|
if ($warning !== '' && $critical !== '') {
|
||||||
|
$insertStmt->execute([
|
||||||
|
':server_id' => $id,
|
||||||
|
':metric_name_id' => $metricType['id'],
|
||||||
|
':warning_threshold' => $warning,
|
||||||
|
':critical_threshold' => $critical,
|
||||||
|
':duration' => $duration
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаемся на страницу сервера
|
||||||
|
return $response->withHeader('Location', "/servers/{$id}")->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveServices(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Получаем список сервисов для мониторинга
|
||||||
|
$services = $params['services'] ?? [];
|
||||||
|
|
||||||
|
if (is_string($services)) {
|
||||||
|
$services = json_decode($services, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем конфигурацию агента
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
||||||
|
VALUES (:server_id, 60, :services, TRUE)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
monitor_services = VALUES(monitor_services),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $id,
|
||||||
|
':services' => json_encode($services)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Возвращаемся на страницу сервера
|
||||||
|
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<?php
|
||||||
|
// src/Controllers/ServerDetailController.php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Model;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class ServerDetailController extends Model
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
|
||||||
|
// Получаем информацию о сервере
|
||||||
|
$stmt = $this->pdo->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 = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем период для выборки метрик
|
||||||
|
$period = $request->getQueryParams()['period'] ?? '24h';
|
||||||
|
|
||||||
|
// Определяем интервал времени в зависимости от периода
|
||||||
|
$interval = match($period) {
|
||||||
|
'7d' => 'INTERVAL 7 DAY',
|
||||||
|
'30d' => 'INTERVAL 30 DAY',
|
||||||
|
default => 'INTERVAL 24 HOUR'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем последние метрики для этого сервера
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT sm.value, mn.name, mn.unit, sm.created_at
|
||||||
|
FROM server_metrics sm
|
||||||
|
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
||||||
|
WHERE sm.server_id = :id
|
||||||
|
AND sm.created_at >= DATE_SUB(NOW(), {$interval})
|
||||||
|
ORDER BY sm.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$metrics = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Группируем метрики по типу
|
||||||
|
$groupedMetrics = [];
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
$name = $metric['name'];
|
||||||
|
if (!isset($groupedMetrics[$name])) {
|
||||||
|
$groupedMetrics[$name] = [];
|
||||||
|
}
|
||||||
|
$groupedMetrics[$name][] = $metric;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем все доступные типы метрик для настройки порогов
|
||||||
|
$stmt = $this->pdo->query("SELECT name, unit FROM metric_names ORDER BY name");
|
||||||
|
$allMetricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Получаем текущие пороговые значения для сервера
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
|
||||||
|
FROM metric_thresholds mt
|
||||||
|
JOIN metric_names mn ON mt.metric_name_id = mn.id
|
||||||
|
WHERE mt.server_id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $id]);
|
||||||
|
$existingThresholds = [];
|
||||||
|
foreach ($stmt->fetchAll() as $threshold) {
|
||||||
|
$existingThresholds[$threshold['name']] = [
|
||||||
|
'warning' => $threshold['warning_threshold'],
|
||||||
|
'critical' => $threshold['critical_threshold'],
|
||||||
|
'duration' => $threshold['duration']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем список сервисов
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT service_name, status, load_state, active_state, sub_state
|
||||||
|
FROM service_status
|
||||||
|
WHERE server_id = :server_id
|
||||||
|
ORDER BY service_name
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
$allServices = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Получаем список сервисов для мониторинга из конфигурации агента
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT monitor_services FROM agent_configs WHERE server_id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
$agentConfig = $stmt->fetch();
|
||||||
|
|
||||||
|
$monitorServices = [];
|
||||||
|
if ($agentConfig && !empty($agentConfig['monitor_services'])) {
|
||||||
|
$monitorServices = json_decode($agentConfig['monitor_services'], true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateData = [
|
||||||
|
'title' => 'Сервер: ' . $server['name'],
|
||||||
|
'server' => $server,
|
||||||
|
'metrics' => $groupedMetrics,
|
||||||
|
'allMetricTypes' => $allMetricTypes,
|
||||||
|
'existingThresholds' => $existingThresholds,
|
||||||
|
'allServices' => $allServices,
|
||||||
|
'period' => $period,
|
||||||
|
'request' => $request->getQueryParams()
|
||||||
|
'monitorServices' => $monitorServices
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveThresholds(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Получаем все типы метрик
|
||||||
|
$stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name");
|
||||||
|
$metricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Удаляем старые пороги для этого сервера
|
||||||
|
$stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id");
|
||||||
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
|
||||||
|
// Добавляем новые пороги
|
||||||
|
$insertStmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration)
|
||||||
|
VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration)
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($metricTypes as $metricType) {
|
||||||
|
$warning = $params[$metricType['name'] . '_warning'] ?? null;
|
||||||
|
$critical = $params[$metricType['name'] . '_critical'] ?? null;
|
||||||
|
$duration = $params[$metricType['name'] . '_duration'] ?? 0;
|
||||||
|
|
||||||
|
// Сохраняем только если указан хотя бы один порог
|
||||||
|
if ($warning !== null || $critical !== null) {
|
||||||
|
$insertStmt->execute([
|
||||||
|
':server_id' => $id,
|
||||||
|
':metric_name_id' => $metricType['id'],
|
||||||
|
':warning_threshold' => $warning,
|
||||||
|
':critical_threshold' => $critical,
|
||||||
|
':duration' => (int)$duration
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаемся на страницу сервера
|
||||||
|
return $response->withHeader('Location', "/servers/{$id}")->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveServices(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
$id = $args['id'];
|
||||||
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Получаем список сервисов для мониторинга
|
||||||
|
$services = $params['services'] ?? [];
|
||||||
|
|
||||||
|
if (is_string($services)) {
|
||||||
|
$services = json_decode($services, true) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем конфигурацию агента
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
||||||
|
VALUES (:server_id, 60, :services, TRUE)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
monitor_services = VALUES(monitor_services),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $id,
|
||||||
|
':services' => json_encode($services)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Возвращаемся на страницу сервера
|
||||||
|
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
// src/Middlewares/AuthMiddleware.php
|
||||||
|
|
||||||
|
namespace App\Middlewares;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
use Slim\Psr7\Response as SlimResponse;
|
||||||
|
|
||||||
|
class AuthMiddleware
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
// Проверяем, авторизован ли пользователь
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
// Если не авторизован, перенаправляем на страницу входа
|
||||||
|
$response = new SlimResponse();
|
||||||
|
return $response
|
||||||
|
->withHeader('Location', '/login')
|
||||||
|
->withStatus(302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если авторизован, продолжаем выполнение
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
// src/Middlewares/CsrfMiddleware.php
|
||||||
|
|
||||||
|
namespace App\Middlewares;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
use Slim\Csrf\Guard;
|
||||||
|
|
||||||
|
class CsrfMiddleware
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
private $csrf;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig, Guard $csrf)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
$this->csrf = $csrf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
// Пропускаем через handler, чтобы Guard мог установить атрибуты
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
// После обработки получаем токены
|
||||||
|
$csrfNameKey = $this->csrf->getTokenNameKey();
|
||||||
|
$csrfValueKey = $this->csrf->getTokenValueKey();
|
||||||
|
|
||||||
|
$csrfName = $request->getAttribute($csrfNameKey);
|
||||||
|
$csrfValue = $request->getAttribute($csrfValueKey);
|
||||||
|
|
||||||
|
$this->twig->getEnvironment()->addGlobal('csrf', [
|
||||||
|
'name_key' => $csrfNameKey,
|
||||||
|
'value_key' => $csrfValueKey,
|
||||||
|
'name' => $csrfName,
|
||||||
|
'value' => $csrfValue
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
// src/Middlewares/SessionMiddleware.php
|
||||||
|
|
||||||
|
namespace App\Middlewares;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class SessionMiddleware
|
||||||
|
{
|
||||||
|
private $twig;
|
||||||
|
|
||||||
|
public function __construct(Twig $twig)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
// Добавляем данные сессии в контекст Twig
|
||||||
|
$sessionData = [
|
||||||
|
'user_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'username' => $_SESSION['username'] ?? null,
|
||||||
|
'role' => $_SESSION['role'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
// Получаем environment и добавляем session в глобальный контекст
|
||||||
|
$environment = $this->twig->getEnvironment();
|
||||||
|
$environment->addGlobal('session', $sessionData);
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
// src/Models/Alert.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Alert
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = DatabaseConfig::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll($resolved = false)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT a.*, s.name as server_name, sg.name as group_name, sg.icon as group_icon
|
||||||
|
FROM alerts a
|
||||||
|
JOIN servers s ON a.server_id = s.id
|
||||||
|
LEFT JOIN server_groups sg ON s.group_id = sg.id
|
||||||
|
WHERE a.resolved = ?
|
||||||
|
ORDER BY a.created_at DESC");
|
||||||
|
$stmt->execute([$resolved ? 1 : 0]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT a.*, s.name as server_name, sg.name as group_name, sg.icon as group_icon
|
||||||
|
FROM alerts a
|
||||||
|
JOIN servers s ON a.server_id = s.id
|
||||||
|
LEFT JOIN server_groups sg ON s.group_id = sg.id
|
||||||
|
WHERE a.id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsResolved($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("UPDATE alerts SET resolved = TRUE, resolved_at = NOW() WHERE id = ?");
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
// src/Models/Group.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Group
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = DatabaseConfig::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll()
|
||||||
|
{
|
||||||
|
$stmt = $this->db->query("SELECT * FROM server_groups ORDER BY name");
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM server_groups WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($data)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("INSERT INTO server_groups (name, description, icon, color)
|
||||||
|
VALUES (?, ?, ?, ?)");
|
||||||
|
return $stmt->execute([
|
||||||
|
$data['name'],
|
||||||
|
$data['description'] ?? null,
|
||||||
|
$data['icon'] ?? null,
|
||||||
|
$data['color'] ?? null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, $data)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("UPDATE server_groups
|
||||||
|
SET name = ?, description = ?, icon = ?, color = ?
|
||||||
|
WHERE id = ?");
|
||||||
|
return $stmt->execute([
|
||||||
|
$data['name'],
|
||||||
|
$data['description'] ?? null,
|
||||||
|
$data['icon'] ?? null,
|
||||||
|
$data['color'] ?? null,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("DELETE FROM server_groups WHERE id = ?");
|
||||||
|
return $stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
// src/Models/Model.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
|
||||||
|
abstract class Model
|
||||||
|
{
|
||||||
|
protected $pdo;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->pdo = DatabaseConfig::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Экранирование данных для безопасности
|
||||||
|
*/
|
||||||
|
protected function sanitize($data)
|
||||||
|
{
|
||||||
|
if (is_array($data)) {
|
||||||
|
return array_map('htmlspecialchars', $data);
|
||||||
|
}
|
||||||
|
return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
// src/Models/Server.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Config\DatabaseConfig;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Server
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->db = DatabaseConfig::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
WHERE s.id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStats()
|
||||||
|
{
|
||||||
|
$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 server_groups");
|
||||||
|
$stats['total_groups'] = $stmt->fetch()['total'];
|
||||||
|
|
||||||
|
// Активные алерты (warning)
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
// src/Models/User.php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
class User extends Model
|
||||||
|
{
|
||||||
|
public function findByUsername($username)
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = :username LIMIT 1");
|
||||||
|
$stmt->execute([':username' => $username]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate($username, $password)
|
||||||
|
{
|
||||||
|
$user = $this->findByUsername($username);
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($username, $password, $email, $role = 'user')
|
||||||
|
{
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
INSERT INTO users (username, password_hash, email, role)
|
||||||
|
VALUES (:username, :password_hash, :email, :role)
|
||||||
|
");
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
':username' => $username,
|
||||||
|
':password_hash' => $passwordHash,
|
||||||
|
':email' => $email,
|
||||||
|
':role' => $role
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
// src/Utils/EncryptionHelper.php
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
class EncryptionHelper
|
||||||
|
{
|
||||||
|
private static $cipher = 'AES-256-CBC';
|
||||||
|
private static $key = 'mon_sys_encryption_key_32_chars!'; // 32-byte key
|
||||||
|
private static $ivLength = 16;
|
||||||
|
|
||||||
|
public static function encrypt($plaintext)
|
||||||
|
{
|
||||||
|
$iv = random_bytes(self::$ivLength);
|
||||||
|
$encrypted = openssl_encrypt($plaintext, self::$cipher, self::$key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
|
||||||
|
if ($encrypted === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($iv . $encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decrypt($ciphertext)
|
||||||
|
{
|
||||||
|
$ciphertext = base64_decode($ciphertext);
|
||||||
|
|
||||||
|
if ($ciphertext === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iv = substr($ciphertext, 0, self::$ivLength);
|
||||||
|
$encrypted = substr($ciphertext, self::$ivLength);
|
||||||
|
|
||||||
|
return openssl_decrypt($encrypted, self::$cipher, self::$key, OPENSSL_RAW_DATA, $iv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-bell"></i> Настройки уведомлений</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/admin/notifications">
|
||||||
|
<h5><i class="fas fa-envelope"></i> Email уведомления</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="smtp_host" class="form-label">SMTP сервер</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_host" name="smtp_host" placeholder="smtp.gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="smtp_port" class="form-label">Порт</label>
|
||||||
|
<input type="number" class="form-control" id="smtp_port" name="smtp_port" placeholder="587">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="smtp_username" class="form-label">Имя пользователя</label>
|
||||||
|
<input type="text" class="form-control" id="smtp_username" name="smtp_username" placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="smtp_password" class="form-label">Пароль</label>
|
||||||
|
<input type="password" class="form-control" id="smtp_password" name="smtp_password" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="smtp_secure" name="smtp_secure">
|
||||||
|
<label class="form-check-label" for="smtp_secure">
|
||||||
|
Использовать безопасное соединение (SSL/TLS)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5><i class="fab fa-telegram"></i> Telegram уведомления</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="telegram_bot_token" class="form-label">Bot Token</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_bot_token" name="telegram_bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrSTUvwxYZ">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="telegram_chat_id" class="form-label">Chat ID</label>
|
||||||
|
<input type="text" class="form-control" id="telegram_chat_id" name="telegram_chat_id" placeholder="-1001234567890">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5><i class="fas fa-mobile-alt"></i> SMS уведомления</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="sms_api_key" class="form-label">API ключ</label>
|
||||||
|
<input type="text" class="form-control" id="sms_api_key" name="sms_api_key" placeholder="API ключ сервиса">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="sms_sender" class="form-label">Отправитель</label>
|
||||||
|
<input type="text" class="form-control" id="sms_sender" name="sms_sender" placeholder="MyCompany">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить настройки
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="fas fa-users-cog"></i> Управление пользователями</h2>
|
||||||
|
<a href="#" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Добавить пользователя
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if users|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Имя пользователя</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Роль</th>
|
||||||
|
<th>Дата создания</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email|default('-') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if user.role == 'admin' %}
|
||||||
|
<span class="badge bg-danger"><i class="fas fa-crown"></i> Администратор</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary"><i class="fas fa-user"></i> Пользователь</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ user.created_at|date('d.m.Y H:i:s') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/users/{{ user.id }}/edit" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Редактировать
|
||||||
|
</a>
|
||||||
|
<form action="/admin/users/{{ user.id }}" method="post" style="display: inline-block;" onsubmit="return confirm('Вы уверены, что хотите удалить этого пользователя?');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> Удалить
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">Пользователи пока не созданы</p>
|
||||||
|
<a href="#" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Создать первого пользователя
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="fas fa-bell"></i> Алерты</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if alerts|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Сервер</th>
|
||||||
|
<th>Метрика</th>
|
||||||
|
<th>Значение</th>
|
||||||
|
<th>Уровень</th>
|
||||||
|
<th>Время</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for alert in alerts %}
|
||||||
|
<tr class="{% if alert.severity == 'critical' %}table-danger{% else %}table-warning{% endif %}">
|
||||||
|
<td>{{ alert.server_name }}</td>
|
||||||
|
<td>{{ alert.metric_name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}</td>
|
||||||
|
<td>{{ alert.value }}{% if alert.metric_name ends_with '_load' or alert.metric_name ends_with '_used' %}%{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
{% if alert.severity == 'critical' %}
|
||||||
|
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle"></i> Критично</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark"><i class="fas fa-exclamation-circle"></i> Предупреждение</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ alert.created_at|date('d.m.Y H:i:s') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/alerts/{{ alert.id }}/resolve" class="btn btn-sm btn-outline-success" onclick="return confirm('Вы уверены, что хотите отметить этот алерт как исправленный?');">
|
||||||
|
<i class="fas fa-check"></i> <span class="d-none d-sm-inline">Исправлено</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-bell fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">Нет активных алертов</p>
|
||||||
|
<p class="text-muted">Все серверы работают в штатном режиме</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2><i class="fas fa-tachometer-alt"></i> Дашборд мониторинга</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-server text-info fa-2x mb-2"></i>
|
||||||
|
<h3>{{ stats.total_servers }}</h3>
|
||||||
|
<p class="text-muted mb-0">Всего серверов</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-chart-line text-success fa-2x mb-2"></i>
|
||||||
|
<h3>{{ stats.servers_with_metrics }}</h3>
|
||||||
|
<p class="text-muted mb-0">С метриками</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2"></i>
|
||||||
|
<h3>{{ stats.alerts_count }}</h3>
|
||||||
|
<p class="text-muted mb-0">Активных алертов</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Servers List -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="mb-0"><i class="fas fa-server"></i> Серверы</h4>
|
||||||
|
<div>
|
||||||
|
<a href="/servers/create" class="btn btn-sm btn-outline-primary me-2">
|
||||||
|
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить сервер</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-list"></i> <span class="d-none d-sm-inline">Все серверы</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if servers|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Адрес</th>
|
||||||
|
<th>Группа</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Последние метрики</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for server in servers %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ server.name }}</strong></td>
|
||||||
|
<td>{{ server.address|default('-') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if server.group_name %}
|
||||||
|
<span class="badge" style="background-color: {{ server.group_color|default('#6c757d') }}">
|
||||||
|
<i class="fas {{ server.group_icon|default('fa-box') }} me-1"></i>{{ server.group_name }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Без группы</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
<span class="badge bg-success">Активен</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Нет метрик</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/{{ server.id }}" class="btn btn-sm btn-outline-info" title="Просмотр">
|
||||||
|
<i class="fas fa-eye"></i> <span class="d-none d-sm-inline">Просмотр</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/{{ server.id }}/edit" class="btn btn-sm btn-outline-primary" title="Редактировать">
|
||||||
|
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-server fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">Серверы пока не добавлены</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить первый сервер</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-plus-circle"></i> Создать группу серверов</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/groups">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Название группы *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Описание</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="icon" class="form-label">Иконка (Font Awesome)</label>
|
||||||
|
<input type="text" class="form-control" id="icon" name="icon" placeholder="например: fa-server">
|
||||||
|
<small class="form-text text-muted">Используйте классы Font Awesome, например: fa-server, fa-desktop</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="color" class="form-label">Цвет</label>
|
||||||
|
<input type="color" class="form-control form-control-color" id="color" name="color">
|
||||||
|
<small class="form-text text-muted">Выберите цвет для обозначения группы</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="/groups" class="btn btn-secondary me-md-2">
|
||||||
|
<i class="fas fa-arrow-left"></i> Назад
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-edit"></i> Редактировать группу "{{ group.name }}"</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/groups/{{ group.id }}">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Название группы *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="{{ group.name }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Описание</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="3">{{ group.description|default('') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="icon" class="form-label">Иконка (Font Awesome)</label>
|
||||||
|
<input type="text" class="form-control" id="icon" name="icon" value="{{ group.icon|default('') }}" placeholder="например: fa-server">
|
||||||
|
<small class="form-text text-muted">Используйте классы Font Awesome, например: fa-server, fa-desktop</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="color" class="form-label">Цвет</label>
|
||||||
|
<input type="color" class="form-control form-control-color" id="color" name="color" value="{{ group.color|default('#007bff') }}">
|
||||||
|
<small class="form-text text-muted">Выберите цвет для обозначения группы</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="/groups" class="btn btn-secondary me-md-2">
|
||||||
|
<i class="fas fa-arrow-left"></i> Назад
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="fas fa-layer-group"></i> Группы серверов</h2>
|
||||||
|
<a href="/groups/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Создать группу</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if groups|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Цвет</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for group in groups %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="fas {{ group.icon|default('fa-box') }}" {% if group.color %}style="color: {{ group.color }}"{% endif %}></i> {{ group.name }}
|
||||||
|
</td>
|
||||||
|
<td>{{ group.description|default('') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if group.color %}
|
||||||
|
<span class="badge" style="background-color: {{ group.color }}">{{ group.color }}</span>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/groups/{{ group.id }}" class="btn btn-sm btn-outline-info me-1">
|
||||||
|
<i class="fas fa-eye"></i> <span class="d-none d-sm-inline">Просмотр</span>
|
||||||
|
</a>
|
||||||
|
<a href="/groups/{{ group.id }}/edit" class="btn btn-sm btn-outline-primary me-1">
|
||||||
|
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||||||
|
</a>
|
||||||
|
<form action="/groups/{{ group.id }}" method="post" style="display: inline-block;" onsubmit="return confirm('Вы уверены, что хотите удалить эту группу?');">
|
||||||
|
<input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
|
||||||
|
<input type="hidden" name="_METHOD" value="DELETE">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> <span class="d-none d-sm-inline">Удалить</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-layer-group fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">Группы серверов пока не созданы</p>
|
||||||
|
<a href="/groups/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Создать первую группу
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2>
|
||||||
|
<i class="fas {{ group.icon }}"></i> {{ group.name }}
|
||||||
|
{% if group.description %}<small class="text-muted">- {{ group.description }}</small>{% endif %}
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<a href="/groups/{{ group.id }}/edit" class="btn btn-outline-primary me-2">
|
||||||
|
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||||||
|
</a>
|
||||||
|
<a href="/groups" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> <span class="d-none d-sm-inline">Назад к группам</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Информация о группе</h4>
|
||||||
|
<p><strong>Описание:</strong> {{ group.description|default('Нет описания') }}</p>
|
||||||
|
<p><strong>Цвет:</strong>
|
||||||
|
{% if group.color %}
|
||||||
|
<span class="badge" style="background-color: {{ group.color }}">{{ group.color }}</span>
|
||||||
|
{% else %}
|
||||||
|
Не указан
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p><strong>Серверов в группе:</strong> {{ servers|length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0"><i class="fas fa-server"></i> Серверы в группе</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if servers|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Адрес</th>
|
||||||
|
<th>Последние метрики</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for server in servers %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ server.name }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{ server.address|default('-') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||||||
|
{% else %}
|
||||||
|
Нет данных
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
<span class="badge bg-success">Активен</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Нет метрик</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/{{ server.id }}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-eye"></i> Просмотр
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-server fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">В этой группе пока нет серверов</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Добавить сервер
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }} - Система мониторинга</title>
|
||||||
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Font Awesome 6 -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.csrf-field {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
{% if session.username %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<i class="fas fa-server"></i> Система мониторинга
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/"><i class="fas fa-tachometer-alt"></i> Дашборд</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/groups"><i class="fas fa-layer-group"></i> Группы серверов</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/servers"><i class="fas fa-server"></i> Серверы</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/alerts"><i class="fas fa-bell"></i> Аллерты</a>
|
||||||
|
</li>
|
||||||
|
{% if session.role == 'admin' %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-cog"></i> Администрирование
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/admin/users"><i class="fas fa-users"></i> Пользователи</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/admin/notifications"><i class="fas fa-bell"></i> Уведомления</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="navbar-text me-3">
|
||||||
|
Привет, {{ session.username|default('Гость') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/logout"><i class="fas fa-sign-out-alt"></i> Выйти</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<main class="container mt-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% if session.username %}
|
||||||
|
<footer class="footer mt-5 py-3 bg-light">
|
||||||
|
<div class="container text-center">
|
||||||
|
<span class="text-muted">© {{ 'now'|date('Y') }} Система мониторинга серверов</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 JS Bundle with Popper -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Auto-add CSRF tokens to all POST forms via AJAX -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get CSRF tokens from API endpoint
|
||||||
|
fetch('/csrf-token')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Add tokens to all POST forms
|
||||||
|
document.querySelectorAll('form[method="post"]').forEach(function(form) {
|
||||||
|
// Add name field
|
||||||
|
var nameInput = document.createElement('input');
|
||||||
|
nameInput.type = 'hidden';
|
||||||
|
nameInput.name = data.name_key;
|
||||||
|
nameInput.value = data.name;
|
||||||
|
form.appendChild(nameInput);
|
||||||
|
|
||||||
|
// Add value field
|
||||||
|
var valueInput = document.createElement('input');
|
||||||
|
valueInput.type = 'hidden';
|
||||||
|
valueInput.name = data.value_key;
|
||||||
|
valueInput.value = data.value;
|
||||||
|
form.appendChild(valueInput);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to get CSRF tokens:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }} - Система мониторинга</title>
|
||||||
|
<!-- Bootstrap 5 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Font Awesome 6 -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container mt-5">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 JS Bundle with Popper -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Auto-add CSRF tokens to all POST forms via AJAX -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get CSRF tokens from API endpoint
|
||||||
|
fetch('/csrf-token')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Add tokens to all POST forms
|
||||||
|
document.querySelectorAll('form[method="post"]').forEach(function(form) {
|
||||||
|
// Add name field
|
||||||
|
var nameInput = document.createElement('input');
|
||||||
|
nameInput.type = 'hidden';
|
||||||
|
nameInput.name = data.name_key;
|
||||||
|
nameInput.value = data.name;
|
||||||
|
form.appendChild(nameInput);
|
||||||
|
|
||||||
|
// Add value field
|
||||||
|
var valueInput = document.createElement('input');
|
||||||
|
valueInput.type = 'hidden';
|
||||||
|
valueInput.name = data.value_key;
|
||||||
|
valueInput.value = data.value;
|
||||||
|
form.appendChild(valueInput);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to get CSRF tokens:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends "login-layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="text-center"><i class="fas fa-sign-in-alt"></i> Вход в систему</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/login" id="loginForm">
|
||||||
|
<input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Имя пользователя</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Пароль</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-lock"></i> Войти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-plus-circle"></i> Добавить сервер</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/servers">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Название сервера *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
<small class="form-text text-muted">Укажите понятное название сервера</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="address" class="form-label">Адрес сервера</label>
|
||||||
|
<input type="text" class="form-control" id="address" name="address" placeholder="IP или домен">
|
||||||
|
<small class="form-text text-muted">IP-адрес или доменное имя сервера (не обязательно)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="group_id" class="form-label">Группа</label>
|
||||||
|
<select class="form-select" id="group_id" name="group_id">
|
||||||
|
<option value="">Не выбрана</option>
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.id }}">{{ group.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">Выберите группу для объединения серверов</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Описание</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||||
|
<small class="form-text text-muted">Дополнительная информация о сервере</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="/servers" class="btn btn-secondary me-md-2">
|
||||||
|
<i class="fas fa-arrow-left"></i> Назад
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-check-circle text-success"></i> Сервер добавлен</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<h4 class="alert-heading">Сервер "{{ server.name }}" успешно добавлен!</h4>
|
||||||
|
<p>Для подключения агента мониторинга используйте следующий токен:</p>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input type="text" class="form-control" value="{{ token }}" readonly onclick="this.select();">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('{{ token }}')">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0 mt-3">Скачайте скрипт установки агента мониторинга:</p>
|
||||||
|
<a href="/agent/install.sh?token={{ token }}" class="btn btn-primary mt-2">
|
||||||
|
<i class="fas fa-download"></i> Скачать install.sh
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5>Инструкция по установке агента:</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Скачайте скрипт установки с помощью кнопки выше</li>
|
||||||
|
<li>Загрузите его на сервер, который вы хотите мониторить</li>
|
||||||
|
<li>Выполните команду: <code>chmod +x install.sh && ./install.sh</code></li>
|
||||||
|
<li>Агент начнет отправлять метрики на сервер мониторинга</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
|
||||||
|
<a href="/servers" class="btn btn-primary me-md-2">
|
||||||
|
<i class="fas fa-server"></i> К списку серверов
|
||||||
|
</a>
|
||||||
|
<a href="/servers/{{ server.id }}/edit" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-edit"></i> Редактировать сервер
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
const originalText = event.target.innerHTML;
|
||||||
|
event.target.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.innerHTML = originalText;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,456 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
{{ server.name }}
|
||||||
|
{% if server.group_name %}
|
||||||
|
<span class="badge ms-2" {% if server.group_color %}style="background-color: {{ server.group_color }}"{% endif %}>
|
||||||
|
<i class="fas {{ server.group_icon|default('fa-box') }} me-1"></i>{{ server.group_name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<a href="/servers/{{ server.id }}/edit" class="btn btn-outline-primary me-2">
|
||||||
|
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> <span class="d-none d-sm-inline">Назад к списку</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Информация о сервере -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Информация о сервере</h5>
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Название:</strong></td>
|
||||||
|
<td>{{ server.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Адрес:</strong></td>
|
||||||
|
<td>{{ server.address|default('-') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Группа:</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if server.group_name %}
|
||||||
|
<i class="fas {{ server.group_icon|default('fa-box') }}" {% if server.group_color %}style="color: {{ server.group_color }}"{% endif %}></i> {{ server.group_name }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Описание:</strong></td>
|
||||||
|
<td>{{ server.description|default('-') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Последние метрики:</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||||||
|
{% else %}
|
||||||
|
Нет данных
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладки -->
|
||||||
|
<ul class="nav nav-tabs" id="serverTabs" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active" id="metrics-tab" data-bs-toggle="tab" data-bs-target="#metrics" type="button" role="tab">
|
||||||
|
<i class="fas fa-chart-line"></i> Метрики
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="services-tab" data-bs-toggle="tab" data-bs-target="#services" type="button" role="tab">
|
||||||
|
<i class="fas fa-cogs"></i> Сервисы
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="thresholds-tab" data-bs-toggle="tab" data-bs-target="#thresholds" type="button" role="tab">
|
||||||
|
<i class="fas fa-bell"></i> Пороги
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Содержимое вкладок -->
|
||||||
|
<div class="tab-content mt-3">
|
||||||
|
<!-- Вкладка "Метрики" -->
|
||||||
|
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="btn-group d-flex" role="group">
|
||||||
|
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if request.query.period == '24h' or request.query.period is empty %}active{% endif %}">
|
||||||
|
24 часа
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if request.query.period == '7d' %}active{% endif %}">
|
||||||
|
7 дней
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if request.query.period == '30d' %}active{% endif %}">
|
||||||
|
30 дней
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for metricName, metricData in metrics %}
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
{{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}
|
||||||
|
{% if metricData[0].unit %}<small class="text-muted">({{ metricData[0].unit }})</small>{% endif %}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if metricData %}
|
||||||
|
<h3 class="text-center text-end">{{ metricData[0].value }}{{ metricData[0].unit|default('') }}</h3>
|
||||||
|
<p class="text-muted text-center mb-2">
|
||||||
|
{{ metricData[0].created_at|date('d.m.Y H:i:s') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- График для метрики -->
|
||||||
|
<div>
|
||||||
|
<canvas id="chart-{{ metricName }}" width="100%" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center text-muted">Нет данных за этот период</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if metrics|length == 0 %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
<i class="fas fa-info-circle"></i> Нет данных о метриках за выбранный период
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка "Сервисы" -->
|
||||||
|
<div class="tab-pane fade" id="services" role="tabpanel">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
<i class="fas fa-cogs"></i> Сервисы сервера
|
||||||
|
{% if allServices is defined %}
|
||||||
|
<small class="text-muted">(найдено: {{ allServices|length }})</small>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="?tab=services" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span class="d-none d-sm-inline">Обновить список</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<form method="post" action="/servers/{{ server.id }}/services">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="selectAllServices">
|
||||||
|
<label class="form-check-label" for="selectAllServices">
|
||||||
|
Выбрать все
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="row" id="servicesList">
|
||||||
|
{% if allServices is defined and allServices|length > 0 %}
|
||||||
|
{# Сортируем сервисы: сначала running, потом stopped, потом unknown, затем по имени #}
|
||||||
|
{% set runningServices = allServices|filter(s => s.status == 'running')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||||||
|
{% set stoppedServices = allServices|filter(s => s.status == 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||||||
|
{% set unknownServices = allServices|filter(s => s.status != 'running' and s.status != 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
|
||||||
|
|
||||||
|
{# Выводим все сервисы по порядку #}
|
||||||
|
{% for service in runningServices %}
|
||||||
|
<div class="col-md-4 col-lg-3 mb-2">
|
||||||
|
<div class="card border-success">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input service-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="service_{{ service.service_name }}"
|
||||||
|
name="services[]"
|
||||||
|
value="{{ service.service_name }}"
|
||||||
|
{% if service.service_name in monitorServices %}
|
||||||
|
checked
|
||||||
|
{% endif %}>
|
||||||
|
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||||||
|
<i class="fas fa-check-circle text-success me-1"></i>
|
||||||
|
{{ service.service_name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-success mb-1">running</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for service in stoppedServices %}
|
||||||
|
<div class="col-md-4 col-lg-3 mb-2">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input service-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="service_{{ service.service_name }}"
|
||||||
|
name="services[]"
|
||||||
|
value="{{ service.service_name }}"
|
||||||
|
{% if service.service_name in monitorServices %}
|
||||||
|
checked
|
||||||
|
{% endif %}>
|
||||||
|
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||||||
|
<i class="fas fa-times-circle text-danger me-1"></i>
|
||||||
|
{{ service.service_name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-danger mb-1">stopped</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for service in unknownServices %}
|
||||||
|
<div class="col-md-4 col-lg-3 mb-2">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input service-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="service_{{ service.service_name }}"
|
||||||
|
name="services[]"
|
||||||
|
value="{{ service.service_name }}"
|
||||||
|
{% if service.service_name in monitorServices %}
|
||||||
|
checked
|
||||||
|
{% endif %}>
|
||||||
|
<label class="form-check-label" for="service_{{ service.service_name }}">
|
||||||
|
<i class="fas fa-question-circle text-warning me-1"></i>
|
||||||
|
{{ service.service_name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-warning mb-1">unknown</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning text-center">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Агент не отправил список сервисов или не установлен
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-center mt-3">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="requestServices()">
|
||||||
|
<i class="fas fa-download"></i> Запросить список сервисов
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить конфигурацию
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка "Пороги" -->
|
||||||
|
<div class="tab-pane fade" id="thresholds" role="tabpanel">
|
||||||
|
<h4>Настройка порогов</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<form method="post" action="/servers/{{ server.id }}/thresholds">
|
||||||
|
{% for metricType in allMetricTypes %}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
{{ metricType.name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}
|
||||||
|
{% if metricType.unit %}<small class="text-muted">({{ metricType.unit }})</small>{% endif %}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Порог предупреждения</label>
|
||||||
|
<input type="number" class="form-control"
|
||||||
|
name="{{ metricType.name }}_warning"
|
||||||
|
step="0.01"
|
||||||
|
{% if existingThresholds[metricType.name].warning is defined %}
|
||||||
|
value="{{ existingThresholds[metricType.name].warning }}"
|
||||||
|
{% endif %}
|
||||||
|
placeholder="80.00">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Порог критический</label>
|
||||||
|
<input type="number" class="form-control"
|
||||||
|
name="{{ metricType.name }}_critical"
|
||||||
|
step="0.01"
|
||||||
|
{% if existingThresholds[metricType.name].critical is defined %}
|
||||||
|
value="{{ existingThresholds[metricType.name].critical }}"
|
||||||
|
{% endif %}
|
||||||
|
placeholder="90.00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label">
|
||||||
|
<i class="fas fa-clock"></i> Длительность превышения (минуты)
|
||||||
|
</label>
|
||||||
|
<input type="number" class="form-control"
|
||||||
|
name="{{ metricType.name }}_duration"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
{% if existingThresholds[metricType.name].duration is defined %}
|
||||||
|
value="{{ existingThresholds[metricType.name].duration }}"
|
||||||
|
{% endif %}
|
||||||
|
placeholder="0 - отправлять алерт сразу">
|
||||||
|
<small class="text-muted">
|
||||||
|
0 = алерт сразу при превышении, >0 = алерт только если превышено дольше указанного времени
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить пороги
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Функция для получения списка сервисов
|
||||||
|
function requestServices() {
|
||||||
|
fetch('/api/v1/agent/{{ server.id }}/services')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.services) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Ошибка получения списка сервисов: ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик кнопки "Выбрать все"
|
||||||
|
document.getElementById('selectAllServices').addEventListener('change', function() {
|
||||||
|
const checkboxes = document.querySelectorAll('.service-checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = this.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Активация нужной вкладки при загрузке
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const activeTab = urlParams.get('tab') || 'metrics';
|
||||||
|
|
||||||
|
// Находим кнопку нужной вкладки и кликаем
|
||||||
|
const tabButton = document.getElementById(activeTab + '-tab');
|
||||||
|
if (tabButton) {
|
||||||
|
tabButton.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Графики метрик
|
||||||
|
{% for metricName, metricData in metrics %}
|
||||||
|
const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}').getContext('2d');
|
||||||
|
|
||||||
|
// Подготовка данных для графика
|
||||||
|
var labels{{ metricName }} = [];
|
||||||
|
var data{{ metricName }} = [];
|
||||||
|
|
||||||
|
{% for metric in metricData|slice(0, 20)|reverse %}
|
||||||
|
labels{{ metricName }}.push('{{ metric.created_at|date('H:i') }}');
|
||||||
|
data{{ metricName }}.push({{ metric.value }});
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
new Chart(ctx{{ metricName|replace({'-': '_', '.': '_'}) }}, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels{{ metricName }},
|
||||||
|
datasets: [{
|
||||||
|
label: '{{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}',
|
||||||
|
data: data{{ metricName }},
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
tension: 0.1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-edit"></i> Редактировать сервер "{{ server.name }}"</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/servers/{{ server.id }}">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Название сервера *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="{{ server.name }}" required>
|
||||||
|
<small class="form-text text-muted">Укажите понятное название сервера</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="address" class="form-label">Адрес сервера</label>
|
||||||
|
<input type="text" class="form-control" id="address" name="address" value="{{ server.address|default('') }}" placeholder="IP или домен">
|
||||||
|
<small class="form-text text-muted">IP-адрес или доменное имя сервера (не обязательно)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="group_id" class="form-label">Группа</label>
|
||||||
|
<select class="form-select" id="group_id" name="group_id">
|
||||||
|
<option value="">Не выбрана</option>
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.id }}" {% if server.group_id == group.id %}selected{% endif %}>{{ group.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">Выберите группу для объединения серверов</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">Описание</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="3">{{ server.description|default('') }}</textarea>
|
||||||
|
<small class="form-text text-muted">Дополнительная информация о сервере</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="/servers" class="btn btn-secondary me-md-2">
|
||||||
|
<i class="fas fa-arrow-left"></i> Назад
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Сохранить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5>Управление агентом мониторинга:</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<a href="/agent/install.sh?server_id={{ server.id }}" class="btn btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-download"></i> Скачать install.sh
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<a href="/servers/{{ server.id }}/regenerate-token" class="btn btn-outline-warning w-100" onclick="return confirm('Вы уверены, что хотите сгенерировать новый токен? Это сделает недействительным старый скрипт установки.');">
|
||||||
|
<i class="fas fa-sync-alt"></i> Сбросить токен
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Если вы потеряли доступ к агенту или хотите создать новый токен безопасности, используйте кнопку "Сбросить токен".</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="fas fa-server"></i> Серверы</h2>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Добавить сервер</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if servers|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Адрес</th>
|
||||||
|
<th>Группа</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Последние метрики</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for server in servers %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ server.name }}</td>
|
||||||
|
<td>{{ server.address|default('-') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if server.group_name %}
|
||||||
|
<i class="fas {{ server.group_icon|default('fa-box') }}" {% if server.group_color %}style="color: {{ server.group_color }}"{% endif %}></i> {{ server.group_name }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ server.description|default('-')|slice(0, 30) ~ (attribute(server, 'description')|length > 30 ? '...' : '') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if server.last_metrics_at %}
|
||||||
|
{{ server.last_metrics_at|date('d.m.Y H:i:s') }}
|
||||||
|
{% else %}
|
||||||
|
Нет данных
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/servers/{{ server.id }}" class="btn btn-sm btn-info" title="Просмотр">
|
||||||
|
<i class="fas fa-eye"></i> <span class="d-none d-sm-inline">Просмотр</span>
|
||||||
|
</a>
|
||||||
|
<a href="/servers/{{ server.id }}/edit" class="btn btn-sm btn-outline-primary" title="Редактировать">
|
||||||
|
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
|
||||||
|
</a>
|
||||||
|
<form action="/servers/{{ server.id }}" method="post" style="display: inline-block;" onsubmit="return confirm('Вы уверены, что хотите удалить этот сервер?');">
|
||||||
|
<input type="hidden" name="{{ csrf.name_key }}" value="{{ csrf.name }}">
|
||||||
|
<input type="hidden" name="{{ csrf.value_key }}" value="{{ csrf.value }}">
|
||||||
|
<input type="hidden" name="_METHOD" value="DELETE">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Удалить">
|
||||||
|
<i class="fas fa-trash"></i> <span class="d-none d-sm-inline">Удалить</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-server fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="lead">Серверы пока не добавлены</p>
|
||||||
|
<a href="/servers/create" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Добавить первый сервер
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="lead">{{ message }}</p>
|
||||||
|
<a href="/" class="btn btn-primary">
|
||||||
|
<i class="fas fa-home"></i> На главную
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in New Issue