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