diff --git a/Dockerfile b/Dockerfile index 2437e2b..7c42fdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,10 @@ RUN useradd -m appuser && \ # Создаем директорию для снимков RUN mkdir -p /app/snapshots && chown -R appuser:appuser /app -USER appuser +# Скрипт для запуска с поддержкой DEBUG_MODE +COPY start.sh /app/start.sh +RUN chmod 755 /app/start.sh EXPOSE 9001 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "9001"] + +CMD ["/app/start.sh"] diff --git a/Makefile b/Makefile index 31250ac..6702a25 100644 --- a/Makefile +++ b/Makefile @@ -121,9 +121,47 @@ env-check: ## Проверить переменные окружения @if [ -f .env ]; then \ echo "$(GREEN)Файл .env найден$(NC)"; \ echo "$(YELLOW)Основные переменные:$(NC)"; \ - grep -E "^(LOGBOARD_PORT|LOGBOARD_USER|LOGBOARD_PASS|SECRET_KEY|ENCRYPTION_KEY)=" .env || echo "$(RED)Переменные не найдены$(NC)"; \ + grep -E "^(LOGBOARD_PORT|LOGBOARD_USER|LOGBOARD_PASS|SECRET_KEY|ENCRYPTION_KEY|DEBUG_MODE)=" .env || echo "$(RED)Переменные не найдены$(NC)"; \ else \ echo "$(RED)Файл .env не найден. Запустите make setup$(NC)"; \ fi +debug-on: ## Включить режим отладки + @echo "$(GREEN)Включение режима отладки...$(NC)" + @if [ -f .env ]; then \ + sed -i 's/^DEBUG_MODE=.*/DEBUG_MODE=true/' .env; \ + echo "$(GREEN)Режим отладки включен!$(NC)"; \ + echo "$(YELLOW)Перезапустите сервисы: make restart$(NC)"; \ + else \ + echo "$(RED)Файл .env не найден. Запустите make setup$(NC)"; \ + fi + +debug-off: ## Выключить режим отладки + @echo "$(GREEN)Выключение режима отладки...$(NC)" + @if [ -f .env ]; then \ + sed -i 's/^DEBUG_MODE=.*/DEBUG_MODE=false/' .env; \ + echo "$(GREEN)Режим отладки выключен!$(NC)"; \ + echo "$(YELLOW)Перезапустите сервисы: make restart$(NC)"; \ + else \ + echo "$(RED)Файл .env не найден. Запустите make setup$(NC)"; \ + fi + +debug-status: ## Показать статус режима отладки + @echo "$(GREEN)Статус режима отладки:$(NC)" + @if [ -f .env ]; then \ + DEBUG_MODE=$$(grep "^DEBUG_MODE=" .env | cut -d'=' -f2 || echo "false"); \ + if [ "$$DEBUG_MODE" = "true" ]; then \ + echo "$(GREEN)DEBUG_MODE: ВКЛЮЧЕН$(NC)"; \ + echo "$(YELLOW)Доступны:$(NC)"; \ + echo "$(YELLOW) - Auto-reload при изменении кода$(NC)"; \ + echo "$(YELLOW) - Swagger UI: /docs$(NC)"; \ + echo "$(YELLOW) - ReDoc: /redoc$(NC)"; \ + else \ + echo "$(RED)DEBUG_MODE: ВЫКЛЮЧЕН$(NC)"; \ + echo "$(YELLOW)Продакшен режим$(NC)"; \ + fi; \ + else \ + echo "$(RED)Файл .env не найден$(NC)"; \ + fi + diff --git a/README.md b/README.md index c74b81b..9beaec2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,30 @@ ## Описание -LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. +LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе. + +### 🎯 **Идеально для локальной разработки** + +LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой: + +- **Второй монитор** - Держите логи всех микросервисов постоянно видимыми +- **Быстрая отладка** - Мгновенный доступ к логам без переключения между терминалами +- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают +- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов + +### 🐳 **Оптимизирован для Docker и Docker Compose** + +Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом: + +- **Автоматическое обнаружение** всех проектов Docker Compose +- **Быстрый просмотр логов** всех контейнеров в проекте +- **Фильтрация по проектам** - легко переключаться между разными проектами +- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров +- **Интеграция с Docker API** - прямая работа с контейнерами + +### 🚀 **Производительность и удобство** + +Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. ### Основные возможности @@ -28,11 +51,36 @@ LogBoard+ - это современная веб-панель для монит ## Скриншоты -### Светлая тема -![Светлая тема](screenshots/light.png) +### 🔐 Страница входа +| Светлая тема | Темная тема | +|--------------|-------------| +| ![Вход - светлая тема](screenshots/login-white.png) | ![Вход - темная тема](screenshots/login-dark.png) | -### Темная тема -![Темная тема](screenshots/dark.png) +### 📊 Основной интерфейс +| Светлая тема | Темная тема | +|--------------|-------------| +| ![Single View - светлая тема](screenshots/single-view-white.png) | ![Single View - темная тема](screenshots/single-view-dark.png) | + +### 🖥️ Multi-view режим +![Multi-view режим](screenshots/multi-view.png) + +### 📋 Карточки контейнеров +![Карточки контейнеров](screenshots/container-cards.png) + +### 📁 Проекты +![Список проектов](screenshots/projects.png) + +### ⚙️ Настройки +![Панель настроек](screenshots/options.png) + +### 🔧 Сворачиваемая боковая панель +![Сворачиваемая боковая панель](screenshots/collapse-sidebar.png) + +### ❓ Справка +![Окно справки](screenshots/help.png) + +### 🚨 Страницы ошибок +![Страницы ошибок](screenshots/error%20pages.png) ## Быстрый старт @@ -74,6 +122,29 @@ LogBoard+ - это современная веб-панель для монит **Важно:** Обязательно измените пароль в продакшене! +### Режим отладки + +Для разработки и тестирования доступен режим отладки: + +```bash +# Включить режим отладки +make debug-on + +# Выключить режим отладки +make debug-off + +# Проверить статус +make debug-status +``` + +**В режиме отладки доступно:** +- **Auto-reload** - автоматическая перезагрузка при изменении кода +- **Swagger UI** - документация API по адресу `/docs` +- **ReDoc** - альтернативная документация по адресу `/redoc` +- **Подробное логирование** - детальные логи для отладки + +**В продакшене обязательно отключите режим отладки!** + ## Архитектура ### Технологический стек diff --git a/app.py b/app.py index 298f6f1..0b75ca9 100644 --- a/app.py +++ b/app.py @@ -50,11 +50,16 @@ ADMIN_PASSWORD = os.getenv("LOGBOARD_PASS", "admin") # Настройки AJAX обновления AJAX_UPDATE_INTERVAL = int(os.getenv("LOGBOARD_AJAX_UPDATE_INTERVAL", "2000")) +# Настройки режима отладки +DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true" + # Инициализация FastAPI app = FastAPI( title="LogBoard+", description="Веб-панель для просмотра логов микросервисов", - version="1.0.0" + version="1.0.0", + docs_url="/docs" if DEBUG_MODE else None, + redoc_url="/redoc" if DEBUG_MODE else None ) @@ -917,6 +922,40 @@ def api_snapshot( url = f"/snapshots/{fname}" return {"file": fname, "url": url} +@app.get("/api/websocket/status") +def api_websocket_status(current_user: str = Depends(get_current_user)): + """Получить статус WebSocket соединений""" + try: + # Проверяем доступность Docker + docker_client.ping() + + # Получаем список активных контейнеров + containers = docker_client.containers.list() + + # Проверяем, есть ли контейнеры для подключения + if containers: + return { + "status": "available", + "message": "WebSocket соединения доступны", + "containers_count": len(containers), + "timestamp": datetime.now().isoformat() + } + else: + return { + "status": "no_containers", + "message": "Нет доступных контейнеров для подключения", + "containers_count": 0, + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + return { + "status": "error", + "message": f"Ошибка проверки WebSocket статуса: {str(e)}", + "containers_count": 0, + "timestamp": datetime.now().isoformat() + } + # WebSocket: verify token (?token=base64(user:pass)) @app.websocket("/ws/logs/{container_id}") async def ws_logs(ws: WebSocket, container_id: str, tail: int = DEFAULT_TAIL, token: Optional[str] = None, @@ -1176,4 +1215,14 @@ async def ws_fan_group(ws: WebSocket, services: str, tail: int = DEFAULT_TAIL, t if __name__ == "__main__": import uvicorn print(f"LogBoard+ http://0.0.0.0:{APP_PORT}") - uvicorn.run(app, host="0.0.0.0", port=APP_PORT) + print(f"Debug mode: {'ON' if DEBUG_MODE else 'OFF'}") + if DEBUG_MODE: + print("Swagger UI: http://0.0.0.0:{}/docs".format(APP_PORT)) + print("ReDoc: http://0.0.0.0:{}/redoc".format(APP_PORT)) + uvicorn.run( + app, + host="0.0.0.0", + port=APP_PORT, + reload=DEBUG_MODE, + log_level="debug" if DEBUG_MODE else "info" + ) diff --git a/docker-compose.yml b/docker-compose.yml index 0a5f0f0..be55b4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,9 @@ services: # Настройки AJAX обновления LOGBOARD_AJAX_UPDATE_INTERVAL: ${LOGBOARD_AJAX_UPDATE_INTERVAL:-2000} + # Настройки режима отладки + DEBUG_MODE: ${DEBUG_MODE:-false} + ports: - "${LOGBOARD_PORT:-9001}:${LOGBOARD_PORT:-9001}" diff --git a/docs/api.md b/docs/api.md index c4eca52..5a5616f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,7 +14,14 @@ ## Обзор API -LogBoard+ предоставляет REST API и WebSocket API для работы с логами Docker контейнеров. +LogBoard+ предоставляет REST API и WebSocket API для работы с логами Docker контейнеров. API разработан для интеграции с системами мониторинга и автоматизации, а также для создания собственных клиентов. + +### 🎯 **Применение API** + +- **Интеграция с CI/CD** - автоматический мониторинг логов в пайплайнах +- **Собственные дашборды** - создание кастомных интерфейсов мониторинга +- **Автоматизация** - скрипты для анализа логов и алертинга +- **Локальная разработка** - интеграция с IDE и инструментами разработки ### Базовый URL @@ -322,6 +329,33 @@ curl -X GET "http://localhost:9001/api/logs/abc123def456?tail=100&since=2024-01- } ``` +#### GET /api/websocket/status + +Получение статуса WebSocket соединений. + +**Заголовки:** +``` +Authorization: Bearer +``` + +**Ответ:** + +```json +{ + "status": "available", + "message": "WebSocket соединения доступны", + "containers_count": 5, + "timestamp": "2024-01-15T10:30:15.123456" +} +``` + +**Возможные статусы:** +- `available` - WebSocket соединения доступны, есть активные контейнеры +- `no_containers` - Нет доступных контейнеров для подключения +- `error` - Ошибка проверки статуса + +**Примечание:** Когда сервер возвращает статус `available`, клиент всегда показывает `ws: on`, независимо от наличия активных клиентских соединений. + ### Системные #### GET /healthz diff --git a/docs/configuration.md b/docs/configuration.md index 94cd009..43992ae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -97,6 +97,20 @@ LogBoard+ использует переменные окружения для н | `WEB_DESCRIPTION` | Описание веб-интерфейса | `Веб-панель для просмотра логов микросервисов` | Нет | | `WEB_VERSION` | Версия веб-интерфейса | `1.0.0` | Нет | +### Настройки режима отладки + +| Переменная | Описание | По умолчанию | Обязательная | +|------------|----------|--------------|--------------| +| `DEBUG_MODE` | Режим отладки | `false` | Нет | + +**Режим отладки включает:** +- **Auto-reload** - автоматическая перезагрузка при изменении кода +- **Swagger UI** - документация API по адресу `/docs` +- **ReDoc** - альтернативная документация по адресу `/redoc` +- **Подробное логирование** - детальные логи для отладки + +**⚠️ Важно:** В продакшене обязательно установите `DEBUG_MODE=false`! + ### Настройки уведомлений | Переменная | Описание | По умолчанию | Обязательная | @@ -331,6 +345,40 @@ LOG_LEVEL=DEBUG # Безопасность AUTH_ENABLED=true SESSION_TIMEOUT=7200 + +# Режим отладки +DEBUG_MODE=true +``` + +### Режим отладки + +```bash +# .env для режима отладки +LOGBOARD_PORT=9001 +LOGBOARD_USER=admin +LOGBOARD_PASS=dev-password +SECRET_KEY=dev-secret-key +ENCRYPTION_KEY=dev-encryption-key + +# Docker настройки +DOCKER_HOST=unix:///var/run/docker.sock + +# Производительность +LOGBOARD_TAIL=50 +LOGBOARD_AJAX_UPDATE_INTERVAL=500 +LOG_LEVEL=DEBUG + +# Безопасность +AUTH_ENABLED=true +SESSION_TIMEOUT=7200 + +# Режим отладки (ВКЛЮЧЕН) +DEBUG_MODE=true + +# Команды для управления режимом отладки: +# make debug-on # Включить режим отладки +# make debug-off # Выключить режим отладки +# make debug-status # Проверить статус ``` ### Тестирование @@ -386,6 +434,9 @@ SESSION_TIMEOUT=3600 LOG_LEVEL=WARNING LOG_FORMAT=json +# Режим отладки (ОБЯЗАТЕЛЬНО ВЫКЛЮЧЕН в продакшене) +DEBUG_MODE=false + # Уведомления NOTIFICATIONS_ENABLED=true SMTP_HOST=smtp.company.com diff --git a/docs/index.md b/docs/index.md index a3fd0fa..2901ed0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,30 @@ ## Обзор -LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. +LogBoard+ - это современная веб-панель для мониторинга и просмотра логов Docker контейнеров в реальном времени. Приложение идеально подходит для локальной разработки, позволяя разработчикам всегда держать логи микросервисов перед глазами на втором мониторе. + +### 🎯 **Идеально для локальной разработки** + +LogBoard+ особенно полезен для разработчиков, работающих с микросервисной архитектурой: + +- **Второй монитор** - Держите логи всех микросервисов постоянно видимыми +- **Быстрая отладка** - Мгновенный доступ к логам без переключения между терминалами +- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают +- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов + +### 🐳 **Оптимизирован для Docker и Docker Compose** + +Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ станет незаменимым инструментом: + +- **Автоматическое обнаружение** всех проектов Docker Compose +- **Быстрый просмотр логов** всех контейнеров в проекте +- **Фильтрация по проектам** - легко переключаться между разными проектами +- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров +- **Интеграция с Docker API** - прямая работа с контейнерами + +### 🚀 **Производительность и удобство** + +Приложение предоставляет удобный веб-интерфейс для работы с логами микросервисов, поддерживает множественные проекты Docker Compose и включает в себя функции безопасности. ## Быстрый старт @@ -75,6 +98,39 @@ make up - **[env.example](../env.example)** - Пример конфигурации - **[Makefile](../Makefile)** - Команды управления +## Скриншоты + +### 🔐 Страница входа +| Светлая тема | Темная тема | +|--------------|-------------| +| ![Вход - светлая тема](../screenshots/login-white.png) | ![Вход - темная тема](../screenshots/login-dark.png) | + +### 📊 Основной интерфейс +| Светлая тема | Темная тема | +|--------------|-------------| +| ![Single View - светлая тема](../screenshots/single-view-white.png) | ![Single View - темная тема](../screenshots/single-view-dark.png) | + +### 🖥️ Multi-view режим +![Multi-view режим](../screenshots/multi-view.png) + +### 📋 Карточки контейнеров +![Карточки контейнеров](../screenshots/container-cards.png) + +### 📁 Проекты +![Список проектов](../screenshots/projects.png) + +### ⚙️ Настройки +![Панель настроек](../screenshots/options.png) + +### 🔧 Сворачиваемая боковая панель +![Сворачиваемая боковая панель](../screenshots/collapse-sidebar.png) + +### ❓ Справка +![Окно справки](../screenshots/help.png) + +### 🚨 Страницы ошибок +![Страницы ошибок](../screenshots/error%20pages.png) + ## Архитектура ### Технологический стек diff --git a/docs/installation.md b/docs/installation.md index d8d55da..ff59123 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,12 +5,41 @@ ## Содержание -1. [Предварительные требования](#предварительные-требования) -2. [Установка](#установка) -3. [Настройка](#настройка) -4. [Первый запуск](#первый-запуск) -5. [Проверка установки](#проверка-установки) -6. [Настройка для продакшена](#настройка-для-продакшена) +1. [Обзор и применение](#обзор-и-применение) +2. [Предварительные требования](#предварительные-требования) +3. [Установка](#установка) +4. [Настройка](#настройка) +5. [Первый запуск](#первый-запуск) +6. [Проверка установки](#проверка-установки) +7. [Настройка для продакшена](#настройка-для-продакшена) + +## Обзор и применение + +### 🎯 **Идеально для локальной разработки** + +LogBoard+ создан специально для разработчиков, работающих с микросервисной архитектурой. Если вы используете Docker и Docker Compose для локальной разработки, этот инструмент станет незаменимым помощником: + +- **Второй монитор** - Держите логи всех микросервисов постоянно видимыми +- **Быстрая отладка** - Мгновенный доступ к логам без переключения между терминалами +- **Мониторинг в реальном времени** - Видите проблемы сразу, как они возникают +- **Централизованный просмотр** - Все логи в одном месте, а не в десятках терминалов + +### 🐳 **Оптимизирован для Docker и Docker Compose** + +Если ваша инфраструктура основана на Docker и Docker Compose, LogBoard+ предоставляет: + +- **Автоматическое обнаружение** всех проектов Docker Compose +- **Быстрый просмотр логов** всех контейнеров в проекте +- **Фильтрация по проектам** - легко переключаться между разными проектами +- **Multi-view режим** - одновременный просмотр логов нескольких контейнеров +- **Интеграция с Docker API** - прямая работа с контейнерами + +### 💡 **Сценарии использования** + +- **Локальная разработка** - мониторинг логов микросервисов на втором мониторе +- **Отладка проблем** - быстрый поиск ошибок в логах +- **Тестирование** - наблюдение за поведением системы в реальном времени +- **Демонстрации** - показ работы системы клиентам или команде ## Предварительные требования diff --git a/env.example b/env.example index a342a16..59a803c 100644 --- a/env.example +++ b/env.example @@ -87,6 +87,18 @@ WEB_DESCRIPTION=Веб-панель для просмотра логов мик # Версия веб-интерфейса WEB_VERSION=1.0.0 +# ============================================================================= +# НАСТРОЙКИ РЕЖИМА РАЗРАБОТКИ +# ============================================================================= + +# Режим отладки (true/false) +# В режиме отладки: +# - Включен auto-reload страниц при изменении кода +# - Доступна документация Swagger (/docs и /redoc) +# - Подробное логирование +# В продакшене обязательно установите в false +DEBUG_MODE=false + # ============================================================================= # НАСТРОЙКИ ПРОИЗВОДИТЕЛЬНОСТИ # ============================================================================= diff --git a/screenshots/collapse-sidebar.png b/screenshots/collapse-sidebar.png new file mode 100644 index 0000000..aa98e72 Binary files /dev/null and b/screenshots/collapse-sidebar.png differ diff --git a/screenshots/container-cards.png b/screenshots/container-cards.png new file mode 100644 index 0000000..3536cc4 Binary files /dev/null and b/screenshots/container-cards.png differ diff --git a/screenshots/dark.png b/screenshots/dark.png deleted file mode 100644 index 68c3e13..0000000 Binary files a/screenshots/dark.png and /dev/null differ diff --git a/screenshots/error pages.png b/screenshots/error pages.png new file mode 100644 index 0000000..ebf7be7 Binary files /dev/null and b/screenshots/error pages.png differ diff --git a/screenshots/help.png b/screenshots/help.png new file mode 100644 index 0000000..78f083e Binary files /dev/null and b/screenshots/help.png differ diff --git a/screenshots/light.png b/screenshots/light.png deleted file mode 100644 index 9ecf2cf..0000000 Binary files a/screenshots/light.png and /dev/null differ diff --git a/screenshots/login-dark.png b/screenshots/login-dark.png new file mode 100644 index 0000000..8a29b01 Binary files /dev/null and b/screenshots/login-dark.png differ diff --git a/screenshots/login-white.png b/screenshots/login-white.png new file mode 100644 index 0000000..1041c50 Binary files /dev/null and b/screenshots/login-white.png differ diff --git a/screenshots/multi-view.png b/screenshots/multi-view.png new file mode 100644 index 0000000..6fae3bf Binary files /dev/null and b/screenshots/multi-view.png differ diff --git a/screenshots/options.png b/screenshots/options.png new file mode 100644 index 0000000..6678976 Binary files /dev/null and b/screenshots/options.png differ diff --git a/screenshots/projects.png b/screenshots/projects.png new file mode 100644 index 0000000..5a031d6 Binary files /dev/null and b/screenshots/projects.png differ diff --git a/screenshots/single-view-dark.png b/screenshots/single-view-dark.png new file mode 100644 index 0000000..00592f0 Binary files /dev/null and b/screenshots/single-view-dark.png differ diff --git a/screenshots/single-view-white.png b/screenshots/single-view-white.png new file mode 100644 index 0000000..d2dfa28 Binary files /dev/null and b/screenshots/single-view-white.png differ diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..94a1b55 --- /dev/null +++ b/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# LogBoard+ - Скрипт запуска +# Автор: Сергей Антропов +# Сайт: https://devops.org.ru + +set -e + +# Получаем настройки из переменных окружения +DEBUG_MODE=${DEBUG_MODE:-false} +LOGBOARD_PORT=${LOGBOARD_PORT:-9001} + +echo "LogBoard+ Starting..." +echo "Port: $LOGBOARD_PORT" +echo "Debug mode: $DEBUG_MODE" + +# Определяем параметры запуска в зависимости от режима отладки +if [ "$DEBUG_MODE" = "true" ]; then + echo "Starting in DEBUG mode with auto-reload and Swagger docs..." + echo "Swagger UI: http://0.0.0.0:$LOGBOARD_PORT/docs" + echo "ReDoc: http://0.0.0.0:$LOGBOARD_PORT/redoc" + + exec uvicorn app:app \ + --host 0.0.0.0 \ + --port $LOGBOARD_PORT \ + --reload \ + --log-level debug +else + echo "Starting in PRODUCTION mode..." + + exec uvicorn app:app \ + --host 0.0.0.0 \ + --port $LOGBOARD_PORT \ + --log-level info +fi diff --git a/templates/error.html b/templates/error.html index b7fe2a7..4b0de72 100644 --- a/templates/error.html +++ b/templates/error.html @@ -46,12 +46,14 @@ display: flex; align-items: center; justify-content: center; + transition: background-color 0.3s ease, color 0.3s ease; } .error-container { max-width: 600px; padding: 2rem; text-align: center; + transition: all 0.3s ease; } .error-icon { @@ -130,6 +132,34 @@ flex-wrap: wrap; } + .theme-toggle { + position: fixed; + top: 1rem; + right: 1rem; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 50%; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + font-size: 1.2rem; + color: var(--fg); + z-index: 1000; + } + + .theme-toggle:hover { + background: var(--border); + transform: scale(1.1); + } + + .theme-toggle:active { + transform: scale(0.95); + } + .auth-notice { background: var(--warn); color: var(--bg); @@ -178,10 +208,23 @@ width: 100%; max-width: 300px; } + + .theme-toggle { + top: 0.5rem; + right: 0.5rem; + width: 40px; + height: 40px; + font-size: 1rem; + } } + + +
{% if error_code == 401 %} @@ -239,12 +282,31 @@ const savedTheme = localStorage.getItem('lb_theme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); + // Функция для обновления иконки темы + function updateThemeIcon() { + const themeToggle = document.querySelector('.theme-toggle'); + const currentTheme = document.documentElement.getAttribute('data-theme'); + + if (themeToggle) { + if (currentTheme === 'light') { + themeToggle.innerHTML = '🌙'; + themeToggle.title = 'Переключить на темную тему (Ctrl+T)'; + } else { + themeToggle.innerHTML = '☀️'; + themeToggle.title = 'Переключить на светлую тему (Ctrl+T)'; + } + } + } + // Переключатель темы function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('lb_theme', newTheme); + + // Обновляем иконку + updateThemeIcon(); } // Добавляем обработчик клавиш для переключения темы (Ctrl+T) @@ -254,6 +316,11 @@ toggleTheme(); } }); + + // Инициализируем иконку при загрузке страницы + document.addEventListener('DOMContentLoaded', function() { + updateThemeIcon(); + }); diff --git a/templates/index.html b/templates/index.html index 883a004..5aad301 100644 --- a/templates/index.html +++ b/templates/index.html @@ -786,22 +786,22 @@ a{color:var(--link)} /* Компактные контролы в header: по умолчанию скрыты */ .header-compact-controls { display: none; align-items: center; gap: 6px; } -/* Tooltip для кнопки помощи */ +/* Модальное окно для кнопки помощи */ .help-tooltip { - position: absolute; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; background: var(--bg); - border: 1px solid var(--border); - border-radius: 8px; - padding: 16px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - max-width: 320px; - font-size: 13px; - line-height: 1.5; + z-index: 10000; opacity: 0; visibility: hidden; - transition: all 0.2s ease; + transition: all 0.3s ease; pointer-events: none; + display: flex; + flex-direction: column; + overflow-y: auto; } .help-tooltip.show { @@ -813,10 +813,34 @@ a{color:var(--link)} .help-tooltip-header { display: flex; align-items: center; - gap: 8px; - margin-bottom: 12px; - padding-bottom: 8px; + justify-content: space-between; + padding: 20px 30px; border-bottom: 1px solid var(--border); + background: var(--panel); + flex-shrink: 0; +} + +.help-tooltip-header-content { + display: flex; + align-items: center; + gap: 12px; +} + +.help-tooltip-close { + background: var(--chip); + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px 12px; + color: var(--muted); + cursor: pointer; + transition: all 0.2s ease; + font-size: 16px; +} + +.help-tooltip-close:hover { + background: var(--tab-active); + color: var(--fg); + border-color: var(--accent); } .help-tooltip-logo { @@ -835,11 +859,15 @@ a{color:var(--link)} .help-tooltip-title { font-weight: 600; color: var(--fg); - font-size: 14px; + font-size: 18px; } .help-tooltip-section { - margin-bottom: 12px; + margin-bottom: 24px; + padding: 20px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; } .help-tooltip-section:last-child { @@ -848,9 +876,9 @@ a{color:var(--link)} .help-tooltip-section-title { font-weight: 600; - color: var(--fg); - margin-bottom: 6px; - font-size: 12px; + color: var(--accent); + margin-bottom: 12px; + font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; } @@ -861,38 +889,71 @@ a{color:var(--link)} line-height: 1.4; } +.help-tooltip-body { + flex: 1; + padding: 30px; + overflow-y: auto; +} + +.help-tooltip-content-wrapper { + display: flex; + gap: 40px; + max-width: 1200px; + margin: 0 auto; +} + +.help-tooltip-left-column, +.help-tooltip-right-column { + flex: 1; + min-width: 0; +} + .help-tooltip-hotkeys { display: grid; grid-template-columns: auto 1fr; - gap: 8px 12px; - font-size: 11px; - margin-top: 4px; + gap: 12px 20px; + font-size: 12px; + margin-top: 8px; } .help-tooltip-hotkey { background: var(--chip); border: 1px solid var(--border); - border-radius: 4px; - padding: 2px 6px; + border-radius: 6px; + padding: 6px 12px; font-family: 'Courier New', monospace; font-weight: 600; color: var(--fg); text-align: center; - min-width: 40px; + min-width: 60px; + font-size: 11px; } .help-tooltip-description { color: var(--muted); - font-size: 11px; + font-size: 12px; + line-height: 1.4; +} + +.help-tooltip-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 30px; + border-top: 1px solid var(--border); + background: var(--panel); + flex-shrink: 0; } .help-tooltip-author { - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid var(--border); - font-size: 11px; + font-size: 12px; color: var(--muted); - text-align: center; +} + +.help-tooltip-version { + font-size: 12px; + color: var(--accent); + font-weight: 500; } .help-tooltip-author a { @@ -992,6 +1053,12 @@ a{color:var(--link)} border-color: #e0af68; } +.ws-status-btn.ws-available { + background: #7aa2f7; + color: white; + border-color: #7aa2f7; +} + /* Кнопка состояния AJAX Update */ .ajax-update-btn { background: var(--chip); @@ -1958,14 +2025,14 @@ a{color:var(--link)} } .multi-view-header { - padding: 12px 16px; + padding: 5px 16px; background: var(--chip); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 12px; - min-height: 40px; + min-height: 16px; } .multi-view-title { @@ -2000,14 +2067,14 @@ a{color:var(--link)} } .single-view-header { - padding: 12px 16px; + padding: 5px 16px; background: var(--chip); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 12px; - min-height: 40px; + min-height: 16px; } .single-view-title { @@ -2730,40 +2797,102 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} - +
- -
LogBoard+
+
+ +
LogBoard+ - Справка
+
+
-
-
О проекте
-
- Веб-интерфейс для мониторинга логов Docker контейнеров в реальном времени с поддержкой мультивыбора и автоматического обновления. +
+
+
+
+
О проекте
+
+ LogBoard+ - веб-интерфейс для мониторинга логов Docker контейнеров в реальном времени с поддержкой мультивыбора и автоматического обновления. + +

🎯 Идеально для локальной разработки:
+ • Второй монитор - логи всегда перед глазами
+ • Быстрая отладка - без переключения терминалов
+ • Мониторинг в реальном времени
+ • Централизованный просмотр всех логов + +

🐳 Оптимизирован для Docker:
+ • Автоматическое обнаружение проектов
+ • Multi-view режим для нескольких контейнеров
+ • Интеграция с Docker API
+ • Поддержка Docker Compose +
+
+ +
+
Горячие клавиши
+
+
[
+
Предыдущий контейнер
+
]
+
Следующий контейнер
+
Ctrl+R
+
Обновить логи
+
Ctrl+K
+
Обновить логи (альтернатива)
+
Ctrl+B
+
Свернуть/развернуть sidebar
+
Ctrl+F
+
Фокус на фильтр
+
+
+
+ +
+
+
Мышь и выделение
+
+
Shift+клик
+
Добавить в мультивыбор
+
Ctrl+клик
+
Добавить в мультивыбор
+
Выделение текста
+
Показать кнопку копирования
+
+
+ +
+
Функции
+
+
Кнопки уровней
+
Фильтрация по типу логов
+
Auto-scroll
+
Автопрокрутка к новым логам
+
Wrap text
+
Перенос длинных строк
+
Auto-update
+
Автообновление логов
+
Refresh
+
Принудительное обновление
+
Clear
+
Очистить логи
+
Download
+
Скачать логи
+
+
+
-
-
Горячие клавиши
-
-
[
-
Предыдущий контейнер
-
]
-
Следующий контейнер
-
Ctrl+R
-
Обновить логи
-
Ctrl+B
-
Свернуть sidebar
-
Ctrl+F
-
Фокус на фильтр
-
Ctrl+Shift+M
-
Мультивыбор контейнеров
+ - -
@@ -2892,10 +3021,13 @@ const els = { })(); function setWsState(s){ + console.log('setWsState: Устанавливаем состояние', s); + console.log('setWsState: Текущие соединения:', Object.keys(state.open)); + els.wsstate.textContent = 'ws: ' + s; // Удаляем все классы состояний - els.wsstate.classList.remove('ws-on', 'ws-off', 'ws-err'); + els.wsstate.classList.remove('ws-on', 'ws-off', 'ws-err', 'ws-available'); // Добавляем соответствующий класс if (s === 'on') { @@ -2904,6 +3036,157 @@ function setWsState(s){ els.wsstate.classList.add('ws-off'); } else if (s === 'err') { els.wsstate.classList.add('ws-err'); + } else if (s === 'available') { + els.wsstate.classList.add('ws-available'); + } +} + +// Функция для определения общего состояния WebSocket соединений +function determineWsState() { + const openConnections = Object.keys(state.open); + + console.log('determineWsState: Проверяем', openConnections.length, 'соединений'); + console.log('determineWsState: Все соединения:', openConnections); + + // Если нет открытых соединений, проверяем сервер через AJAX + if (openConnections.length === 0) { + console.log('determineWsState: Нет соединений, проверяем сервер'); + // Асинхронно проверяем сервер, но возвращаем 'off' для немедленного отображения + // Если сервер доступен, checkWebSocketStatus установит 'on' + setTimeout(() => { + checkWebSocketStatus(); + }, 100); + return 'off'; + } + + // Проверяем состояние всех соединений + let hasActiveConnection = false; + let hasConnecting = false; + let closedConnections = []; + let errorConnections = []; + + for (const id of openConnections) { + const obj = state.open[id]; + if (obj && obj.ws) { + console.log(`determineWsState: Соединение ${id}, readyState:`, obj.ws.readyState, 'WebSocket:', obj.ws); + + if (obj.ws.readyState === WebSocket.OPEN) { + hasActiveConnection = true; + console.log(`determineWsState: Соединение ${id} активно`); + } else if (obj.ws.readyState === WebSocket.CONNECTING) { + hasConnecting = true; + console.log(`determineWsState: Соединение ${id} подключается`); + } else if (obj.ws.readyState === WebSocket.CLOSED || obj.ws.readyState === WebSocket.CLOSING) { + closedConnections.push(id); + console.log(`determineWsState: Соединение ${id} закрыто/закрывается`); + } + } else { + console.log(`determineWsState: Соединение ${id} не найдено или нет WebSocket, obj:`, obj); + closedConnections.push(id); + } + } + + // Удаляем закрытые соединения + closedConnections.forEach(id => { + console.log(`determineWsState: Удаляем закрытое соединение ${id}`); + delete state.open[id]; + }); + + // Если есть активные соединения или есть соединения в процессе установки + if (hasActiveConnection || hasConnecting) { + console.log('determineWsState: Есть активные/подключающиеся соединения, возвращаем on'); + return 'on'; + } else { + console.log('determineWsState: Нет активных соединений, проверяем сервер'); + // Асинхронно проверяем сервер, но возвращаем 'off' для немедленного отображения + // Если сервер доступен, checkWebSocketStatus установит 'on' + setTimeout(() => { + checkWebSocketStatus(); + }, 100); + return 'off'; + } +} + +// Функция для проверки состояния WebSocket через AJAX +async function checkWebSocketStatus() { + try { + const token = localStorage.getItem('access_token'); + if (!token) { + console.log('checkWebSocketStatus: Нет токена, устанавливаем off'); + setWsState('off'); + return; + } + + console.log('checkWebSocketStatus: Отправляем запрос к /api/websocket/status'); + const response = await fetch('/api/websocket/status', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + console.log('checkWebSocketStatus: Получен ответ, статус:', response.status, response.statusText); + + if (response.ok) { + const data = await response.json(); + console.log('checkWebSocketStatus: Получен ответ от сервера:', data); + + if (data.status === 'available') { + // Проверяем активные клиентские соединения + const openConnections = Object.keys(state.open); + let hasActiveConnection = false; + + for (const id of openConnections) { + const obj = state.open[id]; + if (obj && obj.ws && obj.ws.readyState === WebSocket.OPEN) { + hasActiveConnection = true; + break; + } + } + + // Если сервер доступен, всегда показываем 'on' + console.log('checkWebSocketStatus: Сервер доступен, устанавливаем on'); + setWsState('on'); + } else if (data.status === 'no_containers') { + console.log('checkWebSocketStatus: Нет контейнеров, устанавливаем off'); + setWsState('off'); + } else { + console.log('checkWebSocketStatus: Ошибка сервера, устанавливаем err'); + setWsState('err'); + } + } else { + console.log('checkWebSocketStatus: HTTP ошибка, устанавливаем err'); + setWsState('err'); + } + } catch (error) { + console.error('checkWebSocketStatus: Ошибка запроса:', error); + setWsState('err'); + } +} + +// Интервал для автоматической проверки состояния WebSocket +let wsStatusInterval = null; + +// Функция для запуска автоматической проверки состояния WebSocket +function startWebSocketStatusCheck() { + if (wsStatusInterval) { + clearInterval(wsStatusInterval); + } + + // Проверяем каждые 3 секунды + wsStatusInterval = setInterval(() => { + console.log('Автоматическая проверка состояния WebSocket'); + checkWebSocketStatus(); + }, 3000); + + console.log('Запущена автоматическая проверка состояния WebSocket'); +} + +// Функция для остановки автоматической проверки +function stopWebSocketStatusCheck() { + if (wsStatusInterval) { + clearInterval(wsStatusInterval); + wsStatusInterval = null; + console.log('Остановлена автоматическая проверка состояния WebSocket'); } } @@ -4059,55 +4342,29 @@ function positionTooltip(event, tooltip) { -// Функции для help tooltip +// Функции для help modal function showHelpTooltip() { - const helpBtn = document.getElementById('helpBtn'); const helpTooltip = document.getElementById('helpTooltip'); - if (!helpBtn || !helpTooltip) return; + if (!helpTooltip) return; - // Позиционируем tooltip относительно кнопки - positionHelpTooltip(helpBtn, helpTooltip); - - // Показываем tooltip + // Показываем модальное окно helpTooltip.classList.add('show'); + + // Блокируем скролл страницы + document.body.style.overflow = 'hidden'; } function hideHelpTooltip() { const helpTooltip = document.getElementById('helpTooltip'); if (helpTooltip) { helpTooltip.classList.remove('show'); + + // Восстанавливаем скролл страницы + document.body.style.overflow = ''; } } -function positionHelpTooltip(button, tooltip) { - const buttonRect = button.getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Позиционируем tooltip справа от кнопки по умолчанию - let left = buttonRect.right + 8; - let top = buttonRect.top + (buttonRect.height / 2) - (tooltipRect.height / 2); - - // Проверяем, помещается ли tooltip справа - if (left + tooltipRect.width > viewportWidth - 10) { - // Не помещается справа, позиционируем слева - left = buttonRect.left - tooltipRect.width - 8; - } - - // Проверяем, помещается ли tooltip по вертикали - if (top < 10) { - top = 10; - } else if (top + tooltipRect.height > viewportHeight - 10) { - top = viewportHeight - tooltipRect.height - 10; - } - - // Применяем позицию - tooltip.style.left = `${left}px`; - tooltip.style.top = `${top}px`; -} - // Глобальные переменные для выбора контейнеров let lastSelectedContainerId = null; @@ -4149,12 +4406,7 @@ document.addEventListener('click', (event) => { hideMiniContainerTooltip(); } - // Скрытие help tooltip при клике вне его - const helpTooltip = document.getElementById('helpTooltip'); - const helpBtn = document.getElementById('helpBtn'); - if (helpTooltip && !helpTooltip.contains(event.target) && !helpBtn.contains(event.target)) { - hideHelpTooltip(); - } + }); window.addEventListener('resize', () => { @@ -4562,6 +4814,9 @@ function openMultiViewWs(service) { ws.onmessage = (event) => { console.log(`Multi-view WebSocket received message for ${service.name}: ${event.data.substring(0, 100)}...`); + // Устанавливаем состояние 'on' при получении сообщений + setWsState('on'); + const parts = (event.data||'').split(/\r?\n/); // Проверяем на дублирование в исходных данных @@ -4940,6 +5195,7 @@ function openWs(svc, panel){ console.log(`openWs: Created state.open[${id}] with logEl:`, !!logEl, 'wrapEl:', !!wrapEl); ws.onopen = ()=> { + console.log(`WebSocket ${id}: Соединение открыто`); setWsState('on'); // Очищаем сообщение "Connecting..." когда соединение установлено if (state.current && state.current.id === id && els.logContent) { @@ -4949,12 +5205,38 @@ function openWs(svc, panel){ if (obj.logEl) { obj.logEl.innerHTML = ''; } + + // Принудительно проверяем состояние через AJAX через 500мс и 1 секунду + setTimeout(() => { + console.log(`WebSocket ${id}: Принудительная проверка состояния после открытия (500мс)`); + checkWebSocketStatus(); + }, 500); + + setTimeout(() => { + console.log(`WebSocket ${id}: Принудительная проверка состояния после открытия (1с)`); + checkWebSocketStatus(); + }, 1000); + }; + ws.onclose = ()=> { + console.log(`WebSocket ${id}: Соединение закрыто`); + setWsState(determineWsState()); + + // Принудительно проверяем состояние через AJAX через 500мс + setTimeout(() => { + console.log(`WebSocket ${id}: Принудительная проверка состояния после закрытия`); + checkWebSocketStatus(); + }, 500); + }; + ws.onerror = (error)=> { + console.log(`WebSocket ${id}: Ошибка соединения:`, error); + setWsState('err'); }; - ws.onclose = ()=> setWsState(Object.keys(state.open).length? 'on':'off'); - ws.onerror = ()=> setWsState('err'); ws.onmessage = (ev)=>{ console.log(`Received WebSocket message: ${ev.data.substring(0, 100)}...`); + // Устанавливаем состояние 'on' при получении сообщений + setWsState('on'); + const parts = (ev.data||'').split(/\r?\n/); console.log(`openWs: Processing ${parts.length} lines for container ${id}`); @@ -6134,10 +6416,28 @@ function openFanGroup(services){ const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||'')); state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: ('group:'+services.join(','))}; - ws.onopen = ()=> setWsState('on'); - ws.onclose = ()=> setWsState(Object.keys(state.open).length? 'on':'off'); - ws.onerror = ()=> setWsState('err'); + ws.onopen = ()=> { + console.log(`WebSocket ${id}: Соединение открыто`); + setWsState('on'); + }; + ws.onclose = ()=> { + console.log(`WebSocket ${id}: Соединение закрыто`); + setWsState(determineWsState()); + + // Принудительно проверяем состояние через AJAX через 500мс + setTimeout(() => { + console.log(`WebSocket ${id}: Принудительная проверка состояния после закрытия`); + checkWebSocketStatus(); + }, 500); + }; + ws.onerror = (error)=> { + console.log(`WebSocket ${id}: Ошибка соединения:`, error); + setWsState('err'); + }; ws.onmessage = (ev)=>{ + // Устанавливаем состояние 'on' при получении сообщений + setWsState('on'); + const parts = (ev.data||'').split(/\r?\n/); for (let i=0;i { } catch (error) { console.error('Logout error:', error); } finally { + // Останавливаем автоматическую проверку WebSocket + stopWebSocketStatusCheck(); // Очищаем localStorage localStorage.removeItem('access_token'); // Перенаправляем на страницу входа @@ -7144,33 +7446,31 @@ document.addEventListener('DOMContentLoaded', () => { const helpTooltip = document.getElementById('helpTooltip'); let tooltipTimeout; - // Показ tooltip при наведении - els.helpBtn.addEventListener('mouseenter', () => { - clearTimeout(tooltipTimeout); - tooltipTimeout = setTimeout(() => { - showHelpTooltip(); - }, 300); // Задержка 300мс перед показом + // Показ модального окна при клике + els.helpBtn.addEventListener('click', () => { + showHelpTooltip(); }); - // Скрытие tooltip при уходе мыши - els.helpBtn.addEventListener('mouseleave', () => { - clearTimeout(tooltipTimeout); - tooltipTimeout = setTimeout(() => { + // Кнопка закрытия модального окна + const helpTooltipClose = document.getElementById('helpTooltipClose'); + if (helpTooltipClose) { + helpTooltipClose.addEventListener('click', () => { hideHelpTooltip(); - }, 200); // Задержка 200мс перед скрытием - }); + }); + } - // Показ tooltip при наведении на сам tooltip - helpTooltip.addEventListener('mouseenter', () => { - clearTimeout(tooltipTimeout); - }); - - // Скрытие tooltip при уходе мыши с tooltip - helpTooltip.addEventListener('mouseleave', () => { - clearTimeout(tooltipTimeout); - tooltipTimeout = setTimeout(() => { + // Закрытие по Escape + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { hideHelpTooltip(); - }, 200); + } + }); + + // Закрытие по клику вне модального окна + helpTooltip.addEventListener('click', (e) => { + if (e.target === helpTooltip) { + hideHelpTooltip(); + } }); } @@ -7431,13 +7731,117 @@ window.addEventListener('keydown', async (e)=>{ e.preventDefault(); toggleSidebar(); } + + // Фокус на фильтр по Ctrl/Cmd+F + if ((e.ctrlKey || e.metaKey) && (e.key==='f' || e.key==='а')){ + e.preventDefault(); + console.log('Ctrl+F pressed, els.filter:', els.filter); + + // Функция для фокусировки на фильтре + const focusFilter = () => { + // Сначала попробуем использовать els.filter + let filterElement = els.filter; + + // Если els.filter не найден, попробуем найти элемент напрямую + if (!filterElement) { + console.log('els.filter not found, searching directly...'); + filterElement = document.getElementById('filter'); + } + + // Если элемент найден, фокусируемся на нем + if (filterElement) { + console.log('Focusing on filter element:', filterElement); + try { + filterElement.focus(); + filterElement.select(); + console.log('Filter focused successfully'); + } catch (error) { + console.error('Error focusing filter:', error); + } + } else { + console.error('Filter element not found anywhere!'); + // Попробуем еще раз через небольшую задержку + setTimeout(() => { + const retryElement = document.getElementById('filter'); + if (retryElement) { + console.log('Filter found on retry, focusing...'); + retryElement.focus(); + retryElement.select(); + } + }, 100); + } + }; + + // Вызываем функцию фокусировки + focusFilter(); + } + + }); +// Функция для переинициализации элементов +function reinitializeElements() { + // Переинициализируем элементы, которые могут быть не найдены при первой загрузке + els.filter = document.getElementById('filter'); + els.containerList = document.getElementById('containerList'); + els.logContent = document.getElementById('logContent'); + els.mobileToggle = document.getElementById('mobileToggle'); + els.optionsBtn = document.getElementById('optionsBtn'); + els.helpBtn = document.getElementById('helpBtn'); + els.logoutBtn = document.getElementById('logoutBtn'); + els.sidebar = document.getElementById('sidebar'); + els.sidebarToggle = document.getElementById('sidebarToggle'); + els.header = document.getElementById('header'); + + console.log('Elements reinitialized:', { + filter: !!els.filter, + containerList: !!els.containerList, + logContent: !!els.logContent, + sidebar: !!els.sidebar, + sidebarToggle: !!els.sidebarToggle + }); +} + // Инициализация (async function init() { console.log('Initializing LogBoard+...'); + // Переинициализируем элементы + reinitializeElements(); + + // Инициализируем состояние WebSocket + setWsState('off'); + + // Дополнительно инициализируем элементы после полной загрузки DOM + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', reinitializeElements); + } + + // Инициализируем элементы после полной загрузки страницы + window.addEventListener('load', reinitializeElements); + + // Обработчик для правильной очистки при перезагрузке страницы + window.addEventListener('beforeunload', () => { + // Останавливаем автоматическую проверку WebSocket + stopWebSocketStatusCheck(); + + // Закрываем все WebSocket соединения + Object.keys(state.open).forEach(id => { + const obj = state.open[id]; + if (obj && obj.ws) { + try { + obj.ws.close(); + } catch (e) { + // Игнорируем ошибки при закрытии + } + } + }); + + // Очищаем состояние + state.open = {}; + }); + // Проверяем авторизацию const token = localStorage.getItem('access_token'); if (!token) { @@ -7482,6 +7886,23 @@ window.addEventListener('keydown', async (e)=>{ await fetchProjects(); await fetchServices(); + // Проверяем состояние WebSocket после загрузки сервисов + setTimeout(() => { + console.log('Проверка состояния WebSocket после загрузки сервисов'); + setWsState(determineWsState()); + }, 1000); + + // Запускаем автоматическую проверку состояния WebSocket + startWebSocketStatusCheck(); + + // Добавляем обработчик клика для кнопки WebSocket статуса + if (els.wsstate) { + els.wsstate.addEventListener('click', () => { + console.log('Ручная проверка состояния WebSocket'); + checkWebSocketStatus(); + }); + } + // Проверяем, есть ли сохраненный контейнер в localStorage const savedContainerId = getSelectedContainerFromStorage(); if (savedContainerId) {