--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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>