]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Доработка документации добавление схемы БД
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Wed, 24 Dec 2025 09:37:54 +0000 (12:37 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Wed, 24 Dec 2025 09:37:54 +0000 (12:37 +0300)
erp24/controllers/DiagnosticController.php [new file with mode: 0644]
erp24/views/diagnostic/index.php [new file with mode: 0644]

diff --git a/erp24/controllers/DiagnosticController.php b/erp24/controllers/DiagnosticController.php
new file mode 100644 (file)
index 0000000..0e5e6f5
--- /dev/null
@@ -0,0 +1,683 @@
+<?php
+
+namespace app\controllers;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use PhpAmqpLib\Connection\AMQPStreamConnection;
+use Yii;
+use yii\filters\AccessControl;
+use yii\web\Controller;
+use yii\web\Response;
+
+/**
+ * DiagnosticController - диагностика подключений и тестирование .env конфигурации
+ *
+ * Позволяет тестировать:
+ * - Подключение к PostgreSQL (основная БД)
+ * - Подключение к MySQL (вторичная БД)
+ * - Подключение к внешней БД (dbRemote)
+ * - Подключение к RabbitMQ
+ * - Отправку тестовых сообщений в Telegram боты
+ * - Валидацию всех переменных окружения из .env
+ *
+ * ВАЖНО: Доступ только для администраторов с соответствующими правами
+ */
+class DiagnosticController extends Controller
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors(): array
+    {
+        return [
+            'access' => [
+                'class' => AccessControl::class,
+                'rules' => [
+                    [
+                        'allow' => true,
+                        'roles' => ['@'],
+                        'matchCallback' => function () {
+                            // Доступ для: суперадмин (1), администраторы (50), IT (81)
+                            $admin = Yii::$app->user->identity;
+                            if (!$admin) {
+                                return false;
+                            }
+                            // Проверяем group_id
+                            $groupId = $admin->group_id ?? null;
+                            // AdminGroup::GROUP_ADMINISTRATORS = 50, AdminGroup::GROUP_IT = 81
+                            $allowedGroups = [1, 50, 81];
+                            return in_array((int)$groupId, $allowedGroups, true);
+                        }
+                    ],
+                ],
+                'denyCallback' => function () {
+                    if (Yii::$app->user->isGuest) {
+                        return Yii::$app->response->redirect(['/site/login']);
+                    }
+                    throw new \yii\web\ForbiddenHttpException('Доступ запрещён. Требуются права администратора.');
+                },
+            ],
+        ];
+    }
+
+    /**
+     * Главная страница диагностики
+     */
+    public function actionIndex(): string
+    {
+        $envVars = $this->getEnvVariablesStatus();
+        $connections = $this->getConnectionsStatus();
+
+        return $this->render('index', [
+            'envVars' => $envVars,
+            'connections' => $connections,
+        ]);
+    }
+
+    /**
+     * Тестирование PostgreSQL подключения
+     */
+    public function actionTestPostgres(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        try {
+            $db = Yii::$app->db;
+            $startTime = microtime(true);
+            $db->open();
+            $result = $db->createCommand('SELECT version() as version, NOW() as server_time')->queryOne();
+            $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+            return $this->asJson([
+                'success' => true,
+                'message' => 'PostgreSQL подключение успешно',
+                'data' => [
+                    'version' => $result['version'] ?? 'N/A',
+                    'server_time' => $result['server_time'] ?? 'N/A',
+                    'dsn' => $this->maskDsn($db->dsn),
+                    'duration_ms' => $duration,
+                ],
+            ]);
+        } catch (\Exception $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка подключения к PostgreSQL',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Тестирование внешней БД (dbRemote)
+     */
+    public function actionTestDbRemote(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        // Проверяем наличие компонента
+        if (!Yii::$app->has('dbRemote')) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Компонент dbRemote не сконфигурирован',
+                'hint' => 'Установите DB_REMOTE_HOST в .env файле',
+            ]);
+        }
+
+        try {
+            $db = Yii::$app->dbRemote;
+            $startTime = microtime(true);
+            $db->open();
+            $result = $db->createCommand('SELECT VERSION() as version, NOW() as server_time')->queryOne();
+            $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+            return $this->asJson([
+                'success' => true,
+                'message' => 'Подключение к внешней БД успешно',
+                'data' => [
+                    'version' => $result['version'] ?? 'N/A',
+                    'server_time' => $result['server_time'] ?? 'N/A',
+                    'dsn' => $this->maskDsn($db->dsn),
+                    'duration_ms' => $duration,
+                ],
+            ]);
+        } catch (\Exception $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка подключения к внешней БД',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Тестирование RabbitMQ подключения
+     */
+    public function actionTestRabbitmq(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        $host = getenv('RABBIT_HOST') ?: 'localhost';
+        $user = getenv('RABBIT_USER') ?: '';
+        $password = getenv('RABBIT_PASSWORD') ?: '';
+        $port = 5672;
+
+        if (empty($user) || empty($password)) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'RabbitMQ credentials не настроены',
+                'hint' => 'Установите RABBIT_USER и RABBIT_PASSWORD в .env файле',
+            ]);
+        }
+
+        try {
+            $startTime = microtime(true);
+            $connection = new AMQPStreamConnection($host, $port, $user, $password);
+            $channel = $connection->channel();
+
+            // Проверяем очередь telegram-queue
+            $queueInfo = $channel->queue_declare('telegram-queue', true, true, false, false);
+            $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+            $channel->close();
+            $connection->close();
+
+            return $this->asJson([
+                'success' => true,
+                'message' => 'RabbitMQ подключение успешно',
+                'data' => [
+                    'host' => $host,
+                    'port' => $port,
+                    'user' => $user,
+                    'queue_name' => 'telegram-queue',
+                    'messages_count' => $queueInfo[1] ?? 0,
+                    'consumers_count' => $queueInfo[2] ?? 0,
+                    'duration_ms' => $duration,
+                ],
+            ]);
+        } catch (\Exception $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка подключения к RabbitMQ',
+                'error' => $e->getMessage(),
+                'config' => [
+                    'host' => $host,
+                    'port' => $port,
+                    'user' => $user,
+                ],
+            ]);
+        }
+    }
+
+    /**
+     * Тестирование Telegram бота (основной)
+     */
+    public function actionTestTelegram(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        $token = getenv('TELEGRAM_BOT_TOKEN') ?: '';
+
+        if (empty($token)) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'TELEGRAM_BOT_TOKEN не настроен',
+                'hint' => 'Установите TELEGRAM_BOT_TOKEN в .env файле',
+            ]);
+        }
+
+        // Проверяем, что токен не placeholder
+        if (strpos($token, '000000000') === 0) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'TELEGRAM_BOT_TOKEN содержит placeholder значение',
+                'hint' => 'Замените placeholder на реальный токен бота',
+            ]);
+        }
+
+        try {
+            $client = new Client(['timeout' => 10]);
+            $url = "https://api.telegram.org/bot{$token}/getMe";
+
+            $startTime = microtime(true);
+            $response = $client->get($url);
+            $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+            $data = json_decode($response->getBody()->getContents(), true);
+
+            if ($data['ok'] ?? false) {
+                return $this->asJson([
+                    'success' => true,
+                    'message' => 'Telegram бот подключен успешно',
+                    'data' => [
+                        'bot_id' => $data['result']['id'] ?? 'N/A',
+                        'bot_name' => $data['result']['first_name'] ?? 'N/A',
+                        'bot_username' => '@' . ($data['result']['username'] ?? 'N/A'),
+                        'can_read_messages' => $data['result']['can_read_all_group_messages'] ?? false,
+                        'duration_ms' => $duration,
+                    ],
+                ]);
+            }
+
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Telegram API вернул ошибку',
+                'error' => $data['description'] ?? 'Unknown error',
+            ]);
+        } catch (GuzzleException $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка подключения к Telegram API',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Тестирование Telegram Salebot бота
+     */
+    public function actionTestTelegramSalebot(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        $token = getenv('TELEGRAM_BOT_TOKEN_SALEBOT') ?: '';
+
+        if (empty($token)) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'TELEGRAM_BOT_TOKEN_SALEBOT не настроен',
+                'hint' => 'Установите TELEGRAM_BOT_TOKEN_SALEBOT в .env файле',
+            ]);
+        }
+
+        if (strpos($token, '000000000') === 0) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'TELEGRAM_BOT_TOKEN_SALEBOT содержит placeholder значение',
+                'hint' => 'Замените placeholder на реальный токен бота',
+            ]);
+        }
+
+        try {
+            $client = new Client(['timeout' => 10]);
+            $url = "https://api.telegram.org/bot{$token}/getMe";
+
+            $startTime = microtime(true);
+            $response = $client->get($url);
+            $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+            $data = json_decode($response->getBody()->getContents(), true);
+
+            if ($data['ok'] ?? false) {
+                return $this->asJson([
+                    'success' => true,
+                    'message' => 'Telegram Salebot бот подключен успешно',
+                    'data' => [
+                        'bot_id' => $data['result']['id'] ?? 'N/A',
+                        'bot_name' => $data['result']['first_name'] ?? 'N/A',
+                        'bot_username' => '@' . ($data['result']['username'] ?? 'N/A'),
+                        'duration_ms' => $duration,
+                    ],
+                ]);
+            }
+
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Telegram Salebot API вернул ошибку',
+                'error' => $data['description'] ?? 'Unknown error',
+            ]);
+        } catch (GuzzleException $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка подключения к Telegram Salebot API',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Отправка тестового сообщения в Telegram
+     */
+    public function actionSendTestMessage(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        $chatId = Yii::$app->request->post('chat_id');
+        $botType = Yii::$app->request->post('bot_type', 'main'); // main или salebot
+
+        if (empty($chatId)) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Не указан chat_id',
+            ]);
+        }
+
+        $token = $botType === 'salebot'
+            ? getenv('TELEGRAM_BOT_TOKEN_SALEBOT')
+            : getenv('TELEGRAM_BOT_TOKEN');
+
+        if (empty($token) || strpos($token, '000000000') === 0) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Токен бота не настроен или является placeholder',
+            ]);
+        }
+
+        try {
+            $client = new Client(['timeout' => 10]);
+            $url = "https://api.telegram.org/bot{$token}/sendMessage";
+
+            $message = "🔧 *Тестовое сообщение ERP24*\n\n"
+                . "Время: " . date('Y-m-d H:i:s') . "\n"
+                . "Сервер: " . gethostname() . "\n"
+                . "Бот: " . ($botType === 'salebot' ? 'Salebot' : 'Main') . "\n"
+                . "Пользователь: " . (Yii::$app->user->identity->name ?? 'N/A');
+
+            $response = $client->post($url, [
+                'json' => [
+                    'chat_id' => $chatId,
+                    'text' => $message,
+                    'parse_mode' => 'Markdown',
+                ],
+            ]);
+
+            $data = json_decode($response->getBody()->getContents(), true);
+
+            if ($data['ok'] ?? false) {
+                return $this->asJson([
+                    'success' => true,
+                    'message' => 'Тестовое сообщение отправлено успешно',
+                    'data' => [
+                        'message_id' => $data['result']['message_id'] ?? 'N/A',
+                        'chat_id' => $chatId,
+                    ],
+                ]);
+            }
+
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка отправки сообщения',
+                'error' => $data['description'] ?? 'Unknown error',
+            ]);
+        } catch (GuzzleException $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка отправки сообщения',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Тестирование Queue компонента (AMQP)
+     */
+    public function actionTestQueue(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        if (!Yii::$app->has('queue')) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Компонент queue не сконфигурирован',
+            ]);
+        }
+
+        try {
+            $queue = Yii::$app->queue;
+            $dsn = $queue->dsn ?? 'N/A';
+
+            return $this->asJson([
+                'success' => true,
+                'message' => 'Queue компонент сконфигурирован',
+                'data' => [
+                    'dsn' => $this->maskDsn($dsn),
+                    'queue_name' => $queue->queueName ?? 'N/A',
+                    'ttr' => $queue->ttr ?? 'N/A',
+                    'attempts' => $queue->attempts ?? 'N/A',
+                ],
+            ]);
+        } catch (\Exception $e) {
+            return $this->asJson([
+                'success' => false,
+                'message' => 'Ошибка получения конфигурации Queue',
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * Полная проверка всех переменных окружения
+     */
+    public function actionCheckEnv(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        return $this->asJson([
+            'success' => true,
+            'data' => $this->getEnvVariablesStatus(),
+        ]);
+    }
+
+    /**
+     * Запуск всех тестов
+     */
+    public function actionRunAllTests(): Response
+    {
+        Yii::$app->response->format = Response::FORMAT_JSON;
+
+        $results = [];
+
+        // PostgreSQL
+        $pgResult = $this->actionTestPostgres();
+        $results['postgresql'] = json_decode($pgResult->content, true);
+
+        // dbRemote
+        $remoteResult = $this->actionTestDbRemote();
+        $results['db_remote'] = json_decode($remoteResult->content, true);
+
+        // RabbitMQ
+        $rabbitResult = $this->actionTestRabbitmq();
+        $results['rabbitmq'] = json_decode($rabbitResult->content, true);
+
+        // Telegram Main
+        $telegramResult = $this->actionTestTelegram();
+        $results['telegram_main'] = json_decode($telegramResult->content, true);
+
+        // Telegram Salebot
+        $salebotResult = $this->actionTestTelegramSalebot();
+        $results['telegram_salebot'] = json_decode($salebotResult->content, true);
+
+        // Queue
+        $queueResult = $this->actionTestQueue();
+        $results['queue'] = json_decode($queueResult->content, true);
+
+        // Env vars
+        $results['env_variables'] = $this->getEnvVariablesStatus();
+
+        // Summary
+        $successCount = 0;
+        $failCount = 0;
+        foreach (['postgresql', 'db_remote', 'rabbitmq', 'telegram_main', 'telegram_salebot', 'queue'] as $key) {
+            if ($results[$key]['success'] ?? false) {
+                $successCount++;
+            } else {
+                $failCount++;
+            }
+        }
+
+        return $this->asJson([
+            'success' => $failCount === 0,
+            'summary' => [
+                'total' => $successCount + $failCount,
+                'passed' => $successCount,
+                'failed' => $failCount,
+            ],
+            'results' => $results,
+        ]);
+    }
+
+    /**
+     * Получение статуса переменных окружения
+     */
+    private function getEnvVariablesStatus(): array
+    {
+        $required = [
+            'APP_ENV' => 'Окружение приложения',
+            'POSTGRES_PASSWORD' => 'Пароль PostgreSQL',
+            'RABBIT_USER' => 'Пользователь RabbitMQ',
+            'RABBIT_PASSWORD' => 'Пароль RabbitMQ',
+            'TELEGRAM_BOT_TOKEN' => 'Токен Telegram бота',
+            'COOKIE_VALIDATION_KEY' => 'Ключ валидации cookie',
+        ];
+
+        $optional = [
+            'POSTGRES_HOSTNAME' => 'Хост PostgreSQL',
+            'POSTGRES_PORT' => 'Порт PostgreSQL',
+            'POSTGRES_SCHEMA' => 'Схема PostgreSQL',
+            'POSTGRES_USER' => 'Пользователь PostgreSQL',
+            'RABBIT_HOST' => 'Хост RabbitMQ',
+            'TELEGRAM_BOT_TOKEN_SALEBOT' => 'Токен Salebot',
+            'DB_REMOTE_HOST' => 'Хост внешней БД',
+            'DB_REMOTE_PORT' => 'Порт внешней БД',
+            'DB_REMOTE_SCHEMA' => 'Схема внешней БД',
+            'DB_REMOTE_USER' => 'Пользователь внешней БД',
+            'DB_REMOTE_PASSWORD' => 'Пароль внешней БД',
+            'WHATSAPP_API_KEY' => 'API ключ WhatsApp',
+            'YANDEX_MARKET_API_KEY' => 'API ключ Яндекс.Маркет',
+            'SWITCH_USER_COOKIE_PASSWORD' => 'Пароль переключения пользователя',
+            'COOKIE_VALIDATION_KEY_API2' => 'Ключ валидации cookie API2',
+        ];
+
+        $cameras = [];
+        for ($i = 1; $i <= 5; $i++) {
+            $cameras["CAMERA_{$i}_LOGIN"] = "Логин камеры $i";
+            $cameras["CAMERA_{$i}_PASSWORD"] = "Пароль камеры $i";
+        }
+
+        $result = [
+            'required' => [],
+            'optional' => [],
+            'cameras' => [],
+        ];
+
+        foreach ($required as $var => $description) {
+            $value = getenv($var);
+            $result['required'][$var] = [
+                'description' => $description,
+                'set' => $value !== false && $value !== '',
+                'value' => $this->maskValue($var, $value),
+                'is_placeholder' => $this->isPlaceholder($value),
+            ];
+        }
+
+        foreach ($optional as $var => $description) {
+            $value = getenv($var);
+            $result['optional'][$var] = [
+                'description' => $description,
+                'set' => $value !== false && $value !== '',
+                'value' => $this->maskValue($var, $value),
+            ];
+        }
+
+        foreach ($cameras as $var => $description) {
+            $value = getenv($var);
+            $result['cameras'][$var] = [
+                'description' => $description,
+                'set' => $value !== false && $value !== '',
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Получение статуса подключений
+     */
+    private function getConnectionsStatus(): array
+    {
+        return [
+            'postgresql' => [
+                'name' => 'PostgreSQL (основная БД)',
+                'configured' => Yii::$app->has('db'),
+            ],
+            'db_remote' => [
+                'name' => 'MySQL (внешняя БД)',
+                'configured' => Yii::$app->has('dbRemote'),
+            ],
+            'rabbitmq' => [
+                'name' => 'RabbitMQ',
+                'configured' => !empty(getenv('RABBIT_USER')) && !empty(getenv('RABBIT_PASSWORD')),
+            ],
+            'telegram' => [
+                'name' => 'Telegram Bot (основной)',
+                'configured' => !empty(getenv('TELEGRAM_BOT_TOKEN')) && strpos(getenv('TELEGRAM_BOT_TOKEN'), '000000000') !== 0,
+            ],
+            'telegram_salebot' => [
+                'name' => 'Telegram Bot (Salebot)',
+                'configured' => !empty(getenv('TELEGRAM_BOT_TOKEN_SALEBOT')) && strpos(getenv('TELEGRAM_BOT_TOKEN_SALEBOT'), '000000000') !== 0,
+            ],
+            'queue' => [
+                'name' => 'Queue (AMQP)',
+                'configured' => Yii::$app->has('queue'),
+            ],
+        ];
+    }
+
+    /**
+     * Маскирование DSN для безопасного отображения
+     */
+    private function maskDsn(string $dsn): string
+    {
+        // Маскируем пароль в DSN
+        return preg_replace('/:([^:@]+)@/', ':***@', $dsn);
+    }
+
+    /**
+     * Маскирование значений для безопасного отображения
+     */
+    private function maskValue(string $var, $value): string
+    {
+        if ($value === false || $value === '') {
+            return '(не установлено)';
+        }
+
+        $sensitiveVars = ['PASSWORD', 'TOKEN', 'KEY', 'SECRET'];
+        foreach ($sensitiveVars as $sensitive) {
+            if (stripos($var, $sensitive) !== false) {
+                $length = strlen($value);
+                if ($length <= 4) {
+                    return '****';
+                }
+                return substr($value, 0, 4) . str_repeat('*', min($length - 4, 20));
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * Проверка на placeholder значение
+     */
+    private function isPlaceholder($value): bool
+    {
+        if ($value === false || $value === '') {
+            return false;
+        }
+
+        $placeholders = [
+            '000000000',
+            'dev_password',
+            'dev_cookie_key',
+            'dev_rabbit',
+            'change_me',
+        ];
+
+        foreach ($placeholders as $placeholder) {
+            if (stripos($value, $placeholder) !== false) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/erp24/views/diagnostic/index.php b/erp24/views/diagnostic/index.php
new file mode 100644 (file)
index 0000000..55359f2
--- /dev/null
@@ -0,0 +1,824 @@
+<?php
+
+use yii\helpers\Html;
+
+/** @var yii\web\View $this */
+/** @var array $envVars */
+/** @var array $connections */
+
+$this->title = 'Диагностика системы';
+$this->params['breadcrumbs'][] = $this->title;
+?>
+
+<style>
+    .diagnostic-container {
+        max-width: 1400px;
+        margin: 0 auto;
+        padding: 20px;
+    }
+
+    .diagnostic-header {
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        color: white;
+        padding: 30px;
+        border-radius: 10px;
+        margin-bottom: 30px;
+    }
+
+    .diagnostic-header h1 {
+        margin: 0 0 10px 0;
+        font-size: 28px;
+    }
+
+    .diagnostic-header p {
+        margin: 0;
+        opacity: 0.9;
+    }
+
+    .section-card {
+        background: white;
+        border-radius: 10px;
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+        margin-bottom: 20px;
+        overflow: hidden;
+    }
+
+    .section-header {
+        background: #f8f9fa;
+        padding: 15px 20px;
+        border-bottom: 1px solid #e9ecef;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+
+    .section-header h3 {
+        margin: 0;
+        font-size: 18px;
+        color: #333;
+    }
+
+    .section-body {
+        padding: 20px;
+    }
+
+    .test-item {
+        display: flex;
+        align-items: center;
+        padding: 12px 15px;
+        border-bottom: 1px solid #f0f0f0;
+        transition: background 0.2s;
+    }
+
+    .test-item:last-child {
+        border-bottom: none;
+    }
+
+    .test-item:hover {
+        background: #f8f9fa;
+    }
+
+    .test-icon {
+        width: 40px;
+        height: 40px;
+        border-radius: 8px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-right: 15px;
+        font-size: 20px;
+    }
+
+    .test-icon.postgres {
+        background: #336791;
+        color: white;
+    }
+
+    .test-icon.mysql {
+        background: #4479a1;
+        color: white;
+    }
+
+    .test-icon.rabbitmq {
+        background: #ff6600;
+        color: white;
+    }
+
+    .test-icon.telegram {
+        background: #0088cc;
+        color: white;
+    }
+
+    .test-icon.queue {
+        background: #6c757d;
+        color: white;
+    }
+
+    .test-info {
+        flex: 1;
+    }
+
+    .test-name {
+        font-weight: 600;
+        color: #333;
+        margin-bottom: 3px;
+    }
+
+    .test-status {
+        font-size: 12px;
+        color: #666;
+    }
+
+    .test-actions {
+        display: flex;
+        gap: 10px;
+    }
+
+    .btn-test {
+        padding: 8px 16px;
+        border: none;
+        border-radius: 6px;
+        cursor: pointer;
+        font-size: 13px;
+        transition: all 0.2s;
+    }
+
+    .btn-test-primary {
+        background: #007bff;
+        color: white;
+    }
+
+    .btn-test-primary:hover {
+        background: #0056b3;
+    }
+
+    .btn-test-success {
+        background: #28a745;
+        color: white;
+    }
+
+    .btn-test-success:hover {
+        background: #1e7e34;
+    }
+
+    .btn-run-all {
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        color: white;
+        padding: 12px 24px;
+        border: none;
+        border-radius: 8px;
+        font-size: 16px;
+        cursor: pointer;
+        transition: transform 0.2s, box-shadow 0.2s;
+    }
+
+    .btn-run-all:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+    }
+
+    .env-table {
+        width: 100%;
+        border-collapse: collapse;
+    }
+
+    .env-table th,
+    .env-table td {
+        padding: 10px 12px;
+        text-align: left;
+        border-bottom: 1px solid #f0f0f0;
+    }
+
+    .env-table th {
+        background: #f8f9fa;
+        font-weight: 600;
+        font-size: 13px;
+        color: #666;
+    }
+
+    .env-table tr:hover {
+        background: #f8f9fa;
+    }
+
+    .status-badge {
+        display: inline-block;
+        padding: 4px 10px;
+        border-radius: 20px;
+        font-size: 12px;
+        font-weight: 600;
+    }
+
+    .status-badge.success {
+        background: #d4edda;
+        color: #155724;
+    }
+
+    .status-badge.warning {
+        background: #fff3cd;
+        color: #856404;
+    }
+
+    .status-badge.danger {
+        background: #f8d7da;
+        color: #721c24;
+    }
+
+    .status-badge.info {
+        background: #d1ecf1;
+        color: #0c5460;
+    }
+
+    .result-container {
+        margin-top: 15px;
+        padding: 15px;
+        background: #f8f9fa;
+        border-radius: 8px;
+        display: none;
+    }
+
+    .result-container.show {
+        display: block;
+    }
+
+    .result-container pre {
+        margin: 0;
+        white-space: pre-wrap;
+        word-wrap: break-word;
+        font-size: 13px;
+    }
+
+    .telegram-test-form {
+        background: #f8f9fa;
+        padding: 20px;
+        border-radius: 8px;
+        margin-top: 15px;
+    }
+
+    .telegram-test-form label {
+        display: block;
+        margin-bottom: 5px;
+        font-weight: 600;
+        color: #333;
+    }
+
+    .telegram-test-form input,
+    .telegram-test-form select {
+        width: 100%;
+        padding: 10px 12px;
+        border: 1px solid #ddd;
+        border-radius: 6px;
+        margin-bottom: 15px;
+        font-size: 14px;
+    }
+
+    .telegram-test-form input:focus,
+    .telegram-test-form select:focus {
+        outline: none;
+        border-color: #007bff;
+        box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+    }
+
+    .loading {
+        display: inline-block;
+        width: 20px;
+        height: 20px;
+        border: 2px solid #f3f3f3;
+        border-top: 2px solid #007bff;
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+        margin-right: 8px;
+    }
+
+    @keyframes spin {
+        0% {
+            transform: rotate(0deg);
+        }
+
+        100% {
+            transform: rotate(360deg);
+        }
+    }
+
+    .summary-cards {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+        gap: 20px;
+        margin-bottom: 30px;
+    }
+
+    .summary-card {
+        background: white;
+        border-radius: 10px;
+        padding: 20px;
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+        text-align: center;
+    }
+
+    .summary-card .number {
+        font-size: 36px;
+        font-weight: 700;
+        margin-bottom: 5px;
+    }
+
+    .summary-card .label {
+        color: #666;
+        font-size: 14px;
+    }
+
+    .summary-card.success .number {
+        color: #28a745;
+    }
+
+    .summary-card.danger .number {
+        color: #dc3545;
+    }
+
+    .summary-card.warning .number {
+        color: #ffc107;
+    }
+
+    .summary-card.info .number {
+        color: #17a2b8;
+    }
+
+    .collapsible-content {
+        max-height: 0;
+        overflow: hidden;
+        transition: max-height 0.3s ease-out;
+    }
+
+    .collapsible-content.expanded {
+        max-height: 2000px;
+    }
+
+    .toggle-btn {
+        cursor: pointer;
+        user-select: none;
+    }
+
+    .toggle-btn::after {
+        content: '▼';
+        margin-left: 8px;
+        font-size: 12px;
+        transition: transform 0.3s;
+    }
+
+    .toggle-btn.collapsed::after {
+        transform: rotate(-90deg);
+    }
+</style>
+
+<div class="diagnostic-container">
+    <div class="diagnostic-header">
+        <h1>🔧 Диагностика системы ERP24</h1>
+        <p>Тестирование подключений и проверка конфигурации .env</p>
+    </div>
+
+    <!-- Summary Cards -->
+    <div class="summary-cards" id="summary-cards" style="display: none;">
+        <div class="summary-card success">
+            <div class="number" id="passed-count">0</div>
+            <div class="label">Успешно</div>
+        </div>
+        <div class="summary-card danger">
+            <div class="number" id="failed-count">0</div>
+            <div class="label">Ошибки</div>
+        </div>
+        <div class="summary-card warning">
+            <div class="number" id="warning-count">0</div>
+            <div class="label">Предупреждения</div>
+        </div>
+        <div class="summary-card info">
+            <div class="number" id="total-count">0</div>
+            <div class="label">Всего тестов</div>
+        </div>
+    </div>
+
+    <!-- Run All Tests Button -->
+    <div style="text-align: center; margin-bottom: 30px;">
+        <button class="btn-run-all" onclick="runAllTests()">
+            🚀 Запустить все тесты
+        </button>
+    </div>
+
+    <!-- Connection Tests -->
+    <div class="section-card">
+        <div class="section-header">
+            <h3>🔌 Тестирование подключений</h3>
+        </div>
+        <div class="section-body" style="padding: 0;">
+            <!-- PostgreSQL -->
+            <div class="test-item">
+                <div class="test-icon postgres">🐘</div>
+                <div class="test-info">
+                    <div class="test-name">PostgreSQL (основная БД)</div>
+                    <div class="test-status" id="postgres-status">
+                        <?php if ($connections['postgresql']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge danger">Не настроен</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('postgres')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="postgres-result"></div>
+
+            <!-- Remote DB -->
+            <div class="test-item">
+                <div class="test-icon mysql">🗄️</div>
+                <div class="test-info">
+                    <div class="test-name">MySQL (внешняя БД - dbRemote)</div>
+                    <div class="test-status" id="dbremote-status">
+                        <?php if ($connections['db_remote']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge warning">Не настроен (опционально)</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('db-remote')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="db-remote-result"></div>
+
+            <!-- RabbitMQ -->
+            <div class="test-item">
+                <div class="test-icon rabbitmq">🐰</div>
+                <div class="test-info">
+                    <div class="test-name">RabbitMQ</div>
+                    <div class="test-status" id="rabbitmq-status">
+                        <?php if ($connections['rabbitmq']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge danger">Не настроен</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('rabbitmq')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="rabbitmq-result"></div>
+
+            <!-- Queue -->
+            <div class="test-item">
+                <div class="test-icon queue">📬</div>
+                <div class="test-info">
+                    <div class="test-name">Queue (AMQP компонент)</div>
+                    <div class="test-status" id="queue-status">
+                        <?php if ($connections['queue']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge danger">Не настроен</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('queue')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="queue-result"></div>
+        </div>
+    </div>
+
+    <!-- Telegram Tests -->
+    <div class="section-card">
+        <div class="section-header">
+            <h3>📱 Telegram боты</h3>
+        </div>
+        <div class="section-body" style="padding: 0;">
+            <!-- Main Telegram Bot -->
+            <div class="test-item">
+                <div class="test-icon telegram">📱</div>
+                <div class="test-info">
+                    <div class="test-name">Telegram Bot (основной)</div>
+                    <div class="test-status" id="telegram-status">
+                        <?php if ($connections['telegram']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge danger">Не настроен</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('telegram')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="telegram-result"></div>
+
+            <!-- Salebot Telegram Bot -->
+            <div class="test-item">
+                <div class="test-icon telegram">🤖</div>
+                <div class="test-info">
+                    <div class="test-name">Telegram Bot (Salebot)</div>
+                    <div class="test-status" id="telegram-salebot-status">
+                        <?php if ($connections['telegram_salebot']['configured']): ?>
+                            <span class="status-badge info">Сконфигурирован</span>
+                        <?php else: ?>
+                            <span class="status-badge warning">Не настроен (опционально)</span>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="test-actions">
+                    <button class="btn-test btn-test-primary" onclick="testConnection('telegram-salebot')">
+                        Тестировать
+                    </button>
+                </div>
+            </div>
+            <div class="result-container" id="telegram-salebot-result"></div>
+
+            <!-- Send Test Message Form -->
+            <div class="telegram-test-form">
+                <h4 style="margin-top: 0;">📤 Отправить тестовое сообщение</h4>
+                <label for="chat-id">Chat ID:</label>
+                <input type="text" id="chat-id" placeholder="Введите chat_id получателя">
+
+                <label for="bot-type">Бот:</label>
+                <select id="bot-type">
+                    <option value="main">Основной бот</option>
+                    <option value="salebot">Salebot</option>
+                </select>
+
+                <button class="btn-test btn-test-success" onclick="sendTestMessage()">
+                    📤 Отправить тестовое сообщение
+                </button>
+                <div class="result-container" id="send-message-result"></div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Environment Variables -->
+    <div class="section-card">
+        <div class="section-header">
+            <h3 class="toggle-btn" onclick="toggleSection('env-required')">🔐 Обязательные переменные окружения</h3>
+        </div>
+        <div class="collapsible-content expanded" id="env-required">
+            <div class="section-body" style="padding: 0;">
+                <table class="env-table">
+                    <thead>
+                        <tr>
+                            <th>Переменная</th>
+                            <th>Описание</th>
+                            <th>Значение</th>
+                            <th>Статус</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($envVars['required'] as $var => $info): ?>
+                            <tr>
+                                <td><code><?= Html::encode($var) ?></code></td>
+                                <td><?= Html::encode($info['description']) ?></td>
+                                <td><code><?= Html::encode($info['value']) ?></code></td>
+                                <td>
+                                    <?php if (!$info['set']): ?>
+                                        <span class="status-badge danger">Не установлена</span>
+                                    <?php elseif ($info['is_placeholder'] ?? false): ?>
+                                        <span class="status-badge warning">Placeholder</span>
+                                    <?php else: ?>
+                                        <span class="status-badge success">OK</span>
+                                    <?php endif; ?>
+                                </td>
+                            </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
+    <!-- Optional Environment Variables -->
+    <div class="section-card">
+        <div class="section-header">
+            <h3 class="toggle-btn collapsed" onclick="toggleSection('env-optional')">⚙️ Опциональные переменные окружения</h3>
+        </div>
+        <div class="collapsible-content" id="env-optional">
+            <div class="section-body" style="padding: 0;">
+                <table class="env-table">
+                    <thead>
+                        <tr>
+                            <th>Переменная</th>
+                            <th>Описание</th>
+                            <th>Значение</th>
+                            <th>Статус</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($envVars['optional'] as $var => $info): ?>
+                            <tr>
+                                <td><code><?= Html::encode($var) ?></code></td>
+                                <td><?= Html::encode($info['description']) ?></td>
+                                <td><code><?= Html::encode($info['value']) ?></code></td>
+                                <td>
+                                    <?php if ($info['set']): ?>
+                                        <span class="status-badge success">Установлена</span>
+                                    <?php else: ?>
+                                        <span class="status-badge info">Не установлена</span>
+                                    <?php endif; ?>
+                                </td>
+                            </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
+    <!-- Camera Variables -->
+    <div class="section-card">
+        <div class="section-header">
+            <h3 class="toggle-btn collapsed" onclick="toggleSection('env-cameras')">📹 Камеры</h3>
+        </div>
+        <div class="collapsible-content" id="env-cameras">
+            <div class="section-body" style="padding: 0;">
+                <table class="env-table">
+                    <thead>
+                        <tr>
+                            <th>Переменная</th>
+                            <th>Описание</th>
+                            <th>Статус</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php foreach ($envVars['cameras'] as $var => $info): ?>
+                            <tr>
+                                <td><code><?= Html::encode($var) ?></code></td>
+                                <td><?= Html::encode($info['description']) ?></td>
+                                <td>
+                                    <?php if ($info['set']): ?>
+                                        <span class="status-badge success">Установлена</span>
+                                    <?php else: ?>
+                                        <span class="status-badge info">Не установлена</span>
+                                    <?php endif; ?>
+                                </td>
+                            </tr>
+                        <?php endforeach; ?>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+    const endpoints = {
+        'postgres': '<?= \yii\helpers\Url::to(['/diagnostic/test-postgres']) ?>',
+        'db-remote': '<?= \yii\helpers\Url::to(['/diagnostic/test-db-remote']) ?>',
+        'rabbitmq': '<?= \yii\helpers\Url::to(['/diagnostic/test-rabbitmq']) ?>',
+        'telegram': '<?= \yii\helpers\Url::to(['/diagnostic/test-telegram']) ?>',
+        'telegram-salebot': '<?= \yii\helpers\Url::to(['/diagnostic/test-telegram-salebot']) ?>',
+        'queue': '<?= \yii\helpers\Url::to(['/diagnostic/test-queue']) ?>',
+        'send-message': '<?= \yii\helpers\Url::to(['/diagnostic/send-test-message']) ?>',
+        'run-all': '<?= \yii\helpers\Url::to(['/diagnostic/run-all-tests']) ?>'
+    };
+
+    async function testConnection(type) {
+        const resultContainer = document.getElementById(type + '-result');
+        const statusContainer = document.getElementById(type + '-status');
+
+        resultContainer.innerHTML = '<div class="loading"></div> Тестирование...';
+        resultContainer.classList.add('show');
+
+        try {
+            const response = await fetch(endpoints[type]);
+            const data = await response.json();
+
+            let statusBadge = data.success
+                ? '<span class="status-badge success">✓ Успешно</span>'
+                : '<span class="status-badge danger">✗ Ошибка</span>';
+
+            statusContainer.innerHTML = statusBadge;
+
+            let resultHtml = `<strong>${data.message}</strong><br>`;
+            if (data.data) {
+                resultHtml += '<pre>' + JSON.stringify(data.data, null, 2) + '</pre>';
+            }
+            if (data.error) {
+                resultHtml += '<pre style="color: #dc3545;">' + data.error + '</pre>';
+            }
+            if (data.hint) {
+                resultHtml += '<p style="color: #856404; margin-top: 10px;">💡 ' + data.hint + '</p>';
+            }
+
+            resultContainer.innerHTML = resultHtml;
+        } catch (error) {
+            resultContainer.innerHTML = '<pre style="color: #dc3545;">Ошибка запроса: ' + error.message + '</pre>';
+            statusContainer.innerHTML = '<span class="status-badge danger">✗ Ошибка</span>';
+        }
+    }
+
+    async function sendTestMessage() {
+        const chatId = document.getElementById('chat-id').value;
+        const botType = document.getElementById('bot-type').value;
+        const resultContainer = document.getElementById('send-message-result');
+
+        if (!chatId) {
+            resultContainer.innerHTML = '<span style="color: #dc3545;">Введите chat_id</span>';
+            resultContainer.classList.add('show');
+            return;
+        }
+
+        resultContainer.innerHTML = '<div class="loading"></div> Отправка...';
+        resultContainer.classList.add('show');
+
+        try {
+            const formData = new FormData();
+            formData.append('chat_id', chatId);
+            formData.append('bot_type', botType);
+            formData.append('<?= Yii::$app->request->csrfParam ?>', '<?= Yii::$app->request->csrfToken ?>');
+
+            const response = await fetch(endpoints['send-message'], {
+                method: 'POST',
+                body: formData
+            });
+            const data = await response.json();
+
+            let resultHtml = data.success
+                ? '<span style="color: #28a745;">✓ ' + data.message + '</span>'
+                : '<span style="color: #dc3545;">✗ ' + data.message + '</span>';
+
+            if (data.data) {
+                resultHtml += '<pre>' + JSON.stringify(data.data, null, 2) + '</pre>';
+            }
+            if (data.error) {
+                resultHtml += '<pre style="color: #dc3545;">' + data.error + '</pre>';
+            }
+
+            resultContainer.innerHTML = resultHtml;
+        } catch (error) {
+            resultContainer.innerHTML = '<pre style="color: #dc3545;">Ошибка: ' + error.message + '</pre>';
+        }
+    }
+
+    async function runAllTests() {
+        const summaryCards = document.getElementById('summary-cards');
+        summaryCards.style.display = 'grid';
+
+        document.getElementById('passed-count').textContent = '...';
+        document.getElementById('failed-count').textContent = '...';
+        document.getElementById('warning-count').textContent = '...';
+        document.getElementById('total-count').textContent = '...';
+
+        // Run individual tests
+        const tests = ['postgres', 'db-remote', 'rabbitmq', 'telegram', 'telegram-salebot', 'queue'];
+        let passed = 0;
+        let failed = 0;
+
+        for (const test of tests) {
+            await testConnection(test);
+            // Small delay between tests
+            await new Promise(resolve => setTimeout(resolve, 100));
+        }
+
+        // Get summary from run-all endpoint
+        try {
+            const response = await fetch(endpoints['run-all']);
+            const data = await response.json();
+
+            if (data.summary) {
+                document.getElementById('passed-count').textContent = data.summary.passed;
+                document.getElementById('failed-count').textContent = data.summary.failed;
+                document.getElementById('total-count').textContent = data.summary.total;
+
+                // Count warnings (env vars with placeholders)
+                let warnings = 0;
+                if (data.results.env_variables && data.results.env_variables.required) {
+                    for (const key in data.results.env_variables.required) {
+                        if (data.results.env_variables.required[key].is_placeholder) {
+                            warnings++;
+                        }
+                    }
+                }
+                document.getElementById('warning-count').textContent = warnings;
+            }
+        } catch (error) {
+            console.error('Error running all tests:', error);
+        }
+    }
+
+    function toggleSection(sectionId) {
+        const content = document.getElementById(sectionId);
+        const header = content.previousElementSibling.querySelector('.toggle-btn');
+
+        if (content.classList.contains('expanded')) {
+            content.classList.remove('expanded');
+            header.classList.add('collapsed');
+        } else {
+            content.classList.add('expanded');
+            header.classList.remove('collapsed');
+        }
+    }
+</script>