From 749b40a4941e73dfe07fe2790ed9eb67bb2cf056 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Mon, 18 Aug 2025 20:32:46 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20UI/UX=20LogBoard+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена кнопка Update для управления AJAX auto-update - AJAX auto-update включен по умолчанию - Улучшено управление видимостью кнопки Refresh - Переупорядочены кнопки в header (Update, Refresh) - Унифицированы стили кнопок (высота, шрифт, границы) - Добавлен hover эффект для кнопки options с цветом warning - Позиционирование help-btn в свернутом sidebar - Уменьшена ширина свернутого sidebar на 30% - Добавлена логика разворачивания sidebar при клике на options - Отображение внешнего порта в статусе контейнера - Показ 'standalone' для контейнеров без проекта - Обновлена документация Автор: Сергей Антропов Сайт: https://devops.org.ru --- AJAX_UPDATE_ENHANCEMENTS.md | 217 +++++++++++++++++++ BORDER_STYLING_UPDATE.md | 84 ++++++++ BUTTON_REORDERING.md | 96 +++++++++ CHANGELOG.md | 109 ---------- Makefile | 89 -------- README.md | 1 + REFRESH_BUTTON_STYLING.md | 105 ++++++++++ REFRESH_BUTTON_VISIBILITY.md | 196 ++++++++++++++++++ app.py | 5 +- app/docs/ajax-update.md | 288 ++++++++++++-------------- app/scripts/test_ajax_no_history.py | 163 --------------- app/scripts/test_ajax_update.py | 259 ----------------------- app/scripts/test_all_logs.py | 148 ------------- app/scripts/test_color_formatting.py | 166 --------------- app/scripts/test_multi_view_ajax.py | 285 ------------------------- app/scripts/test_multi_view_layout.py | 154 -------------- docker-compose.yml | 10 + env_comparison.md | 153 ++++++++++++++ scripts/generate-compose.py | 111 ---------- templates/index.html | 172 +++++++++++++-- 20 files changed, 1162 insertions(+), 1649 deletions(-) create mode 100644 AJAX_UPDATE_ENHANCEMENTS.md create mode 100644 BORDER_STYLING_UPDATE.md create mode 100644 BUTTON_REORDERING.md delete mode 100644 CHANGELOG.md create mode 100644 REFRESH_BUTTON_STYLING.md create mode 100644 REFRESH_BUTTON_VISIBILITY.md delete mode 100644 app/scripts/test_ajax_no_history.py delete mode 100644 app/scripts/test_ajax_update.py delete mode 100644 app/scripts/test_all_logs.py delete mode 100644 app/scripts/test_color_formatting.py delete mode 100644 app/scripts/test_multi_view_ajax.py delete mode 100644 app/scripts/test_multi_view_layout.py create mode 100644 env_comparison.md delete mode 100644 scripts/generate-compose.py diff --git a/AJAX_UPDATE_ENHANCEMENTS.md b/AJAX_UPDATE_ENHANCEMENTS.md new file mode 100644 index 0000000..d3eec34 --- /dev/null +++ b/AJAX_UPDATE_ENHANCEMENTS.md @@ -0,0 +1,217 @@ +# Улучшения AJAX Auto-update + +## Описание изменений + +Реализованы значительные улучшения системы AJAX auto-update для LogBoard+. + +## Основные изменения + +### 1. AJAX autoupdate по умолчанию включен + +**Изменение:** Значение `ajaxUpdateEnabled` изменено с `false` на `true` + +**Файл:** `templates/index.html` +```javascript +// Было: +let ajaxUpdateEnabled = false; + +// Стало: +let ajaxUpdateEnabled = true; // По умолчанию включен +``` + +**Преимущества:** +- Пользователи сразу получают автоматическое обновление логов +- Не нужно вручную включать функцию при каждом запуске +- Улучшенный пользовательский опыт + +### 2. Добавлена кнопка Update + +**Новая функциональность:** Кнопка update в header для управления AJAX autoupdate + +**Расположение:** Справа от кнопки WebSocket состояния + +**Визуальные состояния:** +- **Зеленая** - AJAX autoupdate включен +- **Красная** - AJAX autoupdate выключен + +**Функциональность:** +- Клик по кнопке переключает состояние AJAX autoupdate +- Автоматическое обновление цвета при изменении состояния +- Интуитивно понятное управление + +### 3. Улучшенное управление кнопками + +**Логика работы:** +- **AJAX autoupdate включен** → Кнопка refresh скрыта, кнопка update зеленая +- **AJAX autoupdate выключен** → Кнопка refresh показана, кнопка update красная + +## Техническая реализация + +### CSS стили + +```css +/* Кнопка состояния AJAX Update */ +.ajax-update-btn { + background: var(--chip); + color: var(--muted); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 12px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + min-width: 60px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.ajax-update-btn.ajax-on { + background: #7ea855; /* Зеленый цвет */ + color: white; + border-color: #7ea855; +} + +.ajax-update-btn.ajax-off { + background: #f7768e; /* Красный цвет */ + color: white; + border-color: #f7768e; +} + +.ajax-update-btn:hover { + opacity: 0.8; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} +``` + +### HTML структура + +```html + + + +``` + +### JavaScript функции + +#### setAjaxUpdateState(enabled) +```javascript +function setAjaxUpdateState(enabled) { + if (els.ajaxUpdateBtn) { + // Удаляем все классы состояний + els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off'); + + // Добавляем соответствующий класс + if (enabled) { + els.ajaxUpdateBtn.classList.add('ajax-on'); + els.ajaxUpdateBtn.textContent = 'update'; + } else { + els.ajaxUpdateBtn.classList.add('ajax-off'); + els.ajaxUpdateBtn.textContent = 'update'; + } + } +} +``` + +#### Обновленная updateRefreshButtonVisibility() +```javascript +function updateRefreshButtonVisibility() { + const refreshButtons = document.querySelectorAll('.log-refresh-btn'); + refreshButtons.forEach(btn => { + if (ajaxUpdateEnabled) { + // Если ajax autoupdate включен, скрываем кнопку refresh + btn.style.display = 'none'; + } else { + // Если ajax autoupdate выключен, показываем кнопку refresh + btn.style.display = 'inline-flex'; + } + }); + + // Обновляем состояние кнопки update + setAjaxUpdateState(ajaxUpdateEnabled); +} +``` + +#### Обработчик клика +```javascript +// Обработчик для кнопки update (AJAX autoupdate toggle) +if (els.ajaxUpdateBtn) { + els.ajaxUpdateBtn.addEventListener('click', () => { + toggleAjaxLogUpdate(); + }); +} +``` + +## Интеграция с существующим кодом + +### Обновленные функции +1. **enableAjaxLogUpdate()** - обновляет состояние кнопки update +2. **disableAjaxLogUpdate()** - обновляет состояние кнопки update +3. **toggleAjaxLogUpdate()** - обновляет состояние кнопки update +4. **initAjaxUpdateCheckbox()** - устанавливает правильное начальное состояние +5. **initAjaxUpdate()** - инициализирует состояние кнопки update + +### Автоматическое обновление состояния +Состояние кнопки update автоматически обновляется в следующих случаях: +- При инициализации AJAX update +- При изменении состояния чекбокса "Auto-update logs" +- При программном включении/выключении AJAX update +- При переключении состояния через функцию toggleAjaxLogUpdate +- При клике по кнопке update + +## Преимущества реализации + +### 1. Улучшенный UX +- Интуитивно понятное управление AJAX autoupdate +- Визуальная обратная связь о состоянии системы +- Быстрый доступ к управлению через кнопку в header + +### 2. Логическая связность +- Кнопка refresh скрыта, когда она не нужна +- Кнопка update показывает актуальное состояние +- Единообразное поведение всех элементов управления + +### 3. Автоматическое управление +- Не требует ручного вмешательства пользователя +- Состояние синхронизировано между всеми элементами +- Корректная работа при всех сценариях использования + +### 4. Совместимость +- Работает с существующим кодом без нарушений +- Поддерживает все режимы просмотра (single-view, multi-view) +- Совместимо с фильтрацией и настройками уровней логирования + +## Тестирование + +### Сценарии тестирования + +1. **Начальное состояние** + - При загрузке страницы AJAX autoupdate должен быть включен + - Кнопка update должна быть зеленой + - Кнопка refresh должна быть скрыта + +2. **Переключение через кнопку update** + - Клик по кнопке update должен переключить состояние + - Цвет кнопки должен измениться (зеленый ↔ красный) + - Видимость кнопки refresh должна измениться + +3. **Переключение через чекбокс** + - Изменение состояния чекбокса "Auto-update logs" должно обновить кнопку update + - Состояние должно синхронизироваться между всеми элементами + +4. **Программное управление** + - Вызов функций enable/disable должен обновить UI + - Состояние должно корректно отображаться + +## Автор + +Сергей Антропов +Сайт: https://devops.org.ru + +## Дата реализации + +2024 год diff --git a/BORDER_STYLING_UPDATE.md b/BORDER_STYLING_UPDATE.md new file mode 100644 index 0000000..b098ecf --- /dev/null +++ b/BORDER_STYLING_UPDATE.md @@ -0,0 +1,84 @@ +# Добавление Border и Box-shadow к кнопке Refresh + +## Описание изменений + +Добавлены border и box-shadow к кнопке refresh в header для полного соответствия стилям кнопки update. + +## Изменения стилей + +### До изменений: +```css +.log-refresh-btn { + border: none; /* Без границы */ + /* нет box-shadow */ +} +``` + +### После изменений: +```css +.log-refresh-btn { + border: 1px solid var(--accent); /* Граница как у update */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Тень как у update */ +} +``` + +## Сравнение кнопок + +### Кнопка Update: +```css +.ajax-update-btn { + border: 1px solid var(--border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} +``` + +### Кнопка Refresh (после изменений): +```css +.log-refresh-btn { + border: 1px solid var(--accent); /* ✅ Совпадает по стилю */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* ✅ Совпадает */ +} +``` + +## Визуальные улучшения + +### Полное единообразие: +- **Одинаковая граница** - 1px solid для обеих кнопок +- **Одинаковая тень** - box-shadow для обеих кнопок +- **Одинаковая высота** - обе кнопки имеют одинаковую высоту +- **Одинаковая ширина** - минимальная ширина 60px для обеих кнопок +- **Одинаковый размер шрифта** - 11px для обеих кнопок +- **Одинаковые отступы** - 6px 12px для обеих кнопок + +### Преимущества: +1. **Визуальная согласованность** - кнопки выглядят как единый набор элементов управления +2. **Профессиональный вид** - границы и тени придают кнопкам более современный вид +3. **Улучшенная читаемость** - тени помогают выделить кнопки на фоне + +## Технические детали + +### Добавленные CSS свойства: +- `border: 1px solid var(--accent)` - граница в цвет акцента +- `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)` - легкая тень + +### Особенности реализации: +- **Цвет границы** - используется `var(--accent)` вместо `var(--border)` для лучшего соответствия цвету кнопки +- **Тень** - идентична тени кнопки update для полного соответствия +- **Совместимость** - работает в обеих темах (светлая/темная) + +## Совместимость + +Изменения стилей не влияют на: +- Функциональность кнопки +- JavaScript обработчики +- Логику показа/скрытия +- Поведение в разных темах (светлая/темная) + +## Автор + +Сергей Антропов +Сайт: https://devops.org.ru + +## Дата реализации + +2024 год diff --git a/BUTTON_REORDERING.md b/BUTTON_REORDERING.md new file mode 100644 index 0000000..580ccf8 --- /dev/null +++ b/BUTTON_REORDERING.md @@ -0,0 +1,96 @@ +# Изменение порядка кнопок в Header + +## Описание изменений + +Изменен порядок кнопок в header для улучшения логической группировки элементов управления. + +## Новый порядок кнопок + +### До изменений: +1. Счетчики логов (DEBUG, INFO, WARN, ERROR, OTHER) +2. **Кнопка Refresh** +3. Переключатель темы (Theme) +4. Кнопка WebSocket состояния (ws: off) +5. **Кнопка Update** + +### После изменений: +1. Счетчики логов (DEBUG, INFO, WARN, ERROR, OTHER) +2. Переключатель темы (Theme) +3. Кнопка WebSocket состояния (ws: off) +4. **Кнопка Update** +5. **Кнопка Refresh** + +## Логика нового порядка + +### Группировка по функциональности: +1. **Счетчики логов** - отображение статистики +2. **Настройки интерфейса** - переключатель темы +3. **Состояние соединений** - WebSocket и AJAX update +4. **Управление обновлением** - кнопки update и refresh + +### Преимущества нового порядка: +- **Логическая группировка** - связанные элементы находятся рядом +- **Улучшенный UX** - пользователь интуитивно понимает назначение кнопок +- **Последовательность действий** - сначала управление состоянием, потом ручное обновление + +## HTML структура + +```html +
+
+ + + + + + +
+ + +
+ Theme + +
+ + + + + + + +
+``` + +## Визуальное представление + +### Header layout: +``` +[DEBUG: 0] [INFO: 0] [WARN: 0] [ERROR: 0] [OTHER: 0] | Theme [☐] | ws: off | update | Refresh +``` + +### Логические группы: +- **Счетчики**: `[DEBUG: 0] [INFO: 0] [WARN: 0] [ERROR: 0] [OTHER: 0]` +- **Настройки**: `Theme [☐]` +- **Состояние**: `ws: off | update` +- **Управление**: `Refresh` + +## Совместимость + +Изменение порядка кнопок не влияет на: +- Функциональность кнопок +- JavaScript обработчики +- CSS стили +- Логику показа/скрытия кнопок + +Все существующие функции продолжают работать без изменений. + +## Автор + +Сергей Антропов +Сайт: https://devops.org.ru + +## Дата реализации + +2024 год diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8210b5e..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,109 +0,0 @@ -# Changelog - -## [1.6.0] - 2024-12-19 - -### Исправлено -- **Отображение длинных названий контейнеров в multi-view режиме:** - - Исправлена проблема с уезжающими вправо кнопками LogLevels при длинных названиях - - Добавлено обрезание длинных названий с многоточием (text-overflow: ellipsis) - - Установлена фиксированная высота заголовков (min-height: 40px) - - Добавлена защита от сжатия кнопок LogLevels (flex-shrink: 0) - - Новый тест `test-multi-view-layout` для проверки отображения - -## [1.5.0] - 2024-12-19 - -### Добавлено -- **Опция "All logs" в AJAX обновлении:** - - Добавлена опция "All logs" в выпадающий список tail lines - - Возможность загружать все доступные логи контейнера через AJAX - - Поддержка параметра `tail=all` в API эндпоинте `/api/logs/{container_id}` - - Новый тест `test-all-logs` для проверки функциональности - -## [1.3.0] - 2024-12-19 - -### Добавлено -- **Улучшенное AJAX обновление логов:** - - Чекбокс "Auto-update logs" в секции Options (включен по умолчанию) - - Настройка интервала обновления через переменную окружения `LOGBOARD_AJAX_UPDATE_INTERVAL` - - Автоматический запуск обновления при включении чекбокса - - Новый API эндпоинт `/api/settings` для получения настроек приложения - - Упрощенный интерфейс управления (убрана кнопка, добавлен чекбокс) - -## [1.2.0] - 2024-12-19 - -### Добавлено -- **AJAX обновление логов:** - - Новый API эндпоинт `/api/logs/{container_id}` для получения логов через AJAX - - Периодическое обновление логов с настраиваемым интервалом (1с, 2с, 5с, 10с, 30с) - - Умное сравнение и добавление только новых логов - - Кнопка "Запустить AJAX" / "Остановить AJAX" в панели Actions - - Автоматическое обновление счетчиков и применение фильтров - - Остановка AJAX обновления при смене контейнера или режима просмотра - -- **API эндпоинты:** - - `GET /api/logs/{container_id}` - получение логов с поддержкой параметров `tail` и `since` - - Возвращает структурированные данные с временными метками и метаданными контейнера - - Поддержка фильтрации по времени для получения только новых логов - -- **Тестирование:** - - Скрипт `app/scripts/test_ajax_update.py` для тестирования AJAX функциональности - - Команда `make test-ajax` для запуска тестов - - Тестирование производительности и корректности работы API - -- **Документация:** - - Подробная документация в `app/docs/ajax-update.md` - - Описание API эндпоинтов с примерами запросов и ответов - - JavaScript API для управления AJAX обновлением - -### Технические детали -- Реализована функция `performAjaxLogUpdate()` для выполнения AJAX запросов -- Функция `appendNewLogs()` для добавления новых логов в DOM -- Управление состоянием через глобальные переменные `ajaxUpdateEnabled`, `lastLogTimestamp` -- Интеграция с существующими функциями фильтрации и подсчета логов -- Обработка ошибок без остановки обновления - -### Преимущества -- Низкая нагрузка на сервер (только новые логи) -- Надежность HTTP архитектуры без WebSocket сложностей -- Гибкость настройки интервала обновления -- Совместимость с существующими фильтрами и счетчиками - -## [1.1.0] - 2024-12-19 - -### Добавлено -- **Горячие клавиши для обновления логов:** - - `Ctrl+R` - обновить логи в Single и Multi View режимах - - `Ctrl+K` - альтернативная комбинация для обновления логов - - `Ctrl+B` - сворачивание/разворачивание sidebar панели - -- **Функциональность сворачивания sidebar и header:** - - Кнопка сворачивания на границе sidebar и основного контента - - Расположение посередине экрана по высоте - - Плавная анимация сворачивания/разворачивания - - Сохранение состояния в localStorage - - Уменьшение ширины sidebar до 60px в свернутом состоянии - - Логотип LogBoard+ в свернутом sidebar (в самом верху) - - Header сворачивается в тонкую полоску 40px с компактными элементами управления - - Кнопка помощи в свернутом sidebar для открытия модального окна с горячими клавишами - -- **Улучшения пользовательского интерфейса:** - - Модальное окно с полным списком горячих клавиш - - Компактный header в свернутом состоянии с фильтром и счетчиками - - Кнопка помощи в свернутом sidebar - - Адаптивный дизайн для мобильных устройств - -### Технические детали -- Добавлены обработчики событий клавиатуры с проверкой фокуса в полях ввода -- Реализована функция `toggleSidebar()` для управления состоянием sidebar -- Добавлена функция `showHotkeysNotification()` для показа подсказок -- CSS анимации для плавных переходов -- Сохранение пользовательских настроек в localStorage -- Логотип отображается в самом верху свернутого sidebar - -### Совместимость -- Все существующие функции сохранены -- Обратная совместимость с предыдущими версиями -- Поддержка как Single View, так и Multi View режимов - -### Автор -Сергей Антропов - https://devops.org.ru diff --git a/Makefile b/Makefile index b8af754..64c8f51 100644 --- a/Makefile +++ b/Makefile @@ -40,15 +40,6 @@ setup: ## Настроить переменные окружения (копир echo "$(YELLOW)Для пересоздания удалите .env и запустите make setup$(NC)"; \ fi -generate: ## Сгенерировать docker-compose.yml из .env файла - @echo "$(GREEN)Генерация docker-compose.yml из .env файла...$(NC)" - @if [ ! -f .env ]; then \ - echo "$(RED)Файл .env не найден! Сначала запустите make setup$(NC)"; \ - exit 1; \ - fi - python3 scripts/generate-compose.py - @echo "$(GREEN)docker-compose.yml сгенерирован успешно!$(NC)" - build: ## Собрать Docker образ @echo "$(GREEN)Сборка Docker образа...$(NC)" docker compose -f $(COMPOSE_FILE) build --no-cache @@ -74,10 +65,6 @@ logs: ## Показать логи сервисов @echo "$(GREEN)Логи сервисов:$(NC)" docker compose -f $(COMPOSE_FILE) logs -f -logs-tail: ## Показать последние 100 строк логов - @echo "$(GREEN)Последние 100 строк логов:$(NC)" - docker compose -f $(COMPOSE_FILE) logs --tail=100 - clean: ## Остановить сервисы и удалить образы @echo "$(RED)Очистка проекта...$(NC)" docker compose -f $(COMPOSE_FILE) down --rmi all --volumes --remove-orphans @@ -97,10 +84,6 @@ start: up ## Алиас для команды up stop: down ## Алиас для команды down -dev: ## Запуск в режиме разработки (с выводом логов) - @echo "$(GREEN)Запуск в режиме разработки...$(NC)" - docker compose -f $(COMPOSE_FILE) up --build - rebuild: ## Пересобрать и запустить сервисы @echo "$(YELLOW)Пересборка и запуск сервисов...$(NC)" docker compose -f $(COMPOSE_FILE) down @@ -109,76 +92,4 @@ rebuild: ## Пересобрать и запустить сервисы @echo "$(GREEN)Сервисы пересобраны и запущены!$(NC)" @echo "$(YELLOW)Приложение доступно по адресу: http://localhost:9001$(NC)" -test-auth: ## Тестирование новой системы авторизации - @echo "$(GREEN)Тестирование системы авторизации...$(NC)" - @if [ ! -f test_auth.py ]; then \ - echo "$(RED)Файл test_auth.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 test_auth.py - @echo "$(GREEN)Тестирование завершено!$(NC)" - -test-ajax: ## Тестирование AJAX обновления логов - @echo "$(GREEN)Тестирование AJAX обновления логов...$(NC)" - @if [ ! -f app/scripts/test_ajax_update.py ]; then \ - echo "$(RED)Файл test_ajax_update.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 app/scripts/test_ajax_update.py - @echo "$(GREEN)Тестирование AJAX завершено!$(NC)" - -test-multi-view-ajax: ## Тестирование AJAX обновления в multi-view режиме - @echo "$(GREEN)Тестирование AJAX обновления в multi-view режиме...$(NC)" - @if [ ! -f app/scripts/test_multi_view_ajax.py ]; then \ - echo "$(RED)Файл test_multi_view_ajax.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 app/scripts/test_multi_view_ajax.py - @echo "$(GREEN)Тестирование multi-view AJAX завершено!$(NC)" - -test-ajax-no-history: ## Тестирование AJAX обновления без загрузки истории - @echo "$(GREEN)Тестирование AJAX обновления без загрузки истории...$(NC)" - @if [ ! -f app/scripts/test_ajax_no_history.py ]; then \ - echo "$(RED)Файл test_ajax_no_history.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 app/scripts/test_ajax_no_history.py - @echo "$(GREEN)Тестирование AJAX без истории завершено!$(NC)" - -test-all-logs: ## Тестирование опции "all logs" в AJAX обновлении - @echo "$(GREEN)Тестирование опции 'all logs' в AJAX обновлении...$(NC)" - @if [ ! -f app/scripts/test_all_logs.py ]; then \ - echo "$(RED)Файл test_all_logs.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 app/scripts/test_all_logs.py - @echo "$(GREEN)Тестирование опции 'all logs' завершено!$(NC)" - -test-multi-view-layout: ## Тестирование отображения длинных названий в multi-view режиме - @echo "$(GREEN)Тестирование отображения длинных названий в multi-view режиме...$(NC)" - @if [ ! -f app/scripts/test_multi_view_layout.py ]; then \ - echo "$(RED)Файл test_multi_view_layout.py не найден!$(NC)"; \ - exit 1; \ - fi - @echo "$(YELLOW)Убедитесь, что сервис запущен: make up$(NC)" - @echo "$(YELLOW)Ожидание запуска сервиса...$(NC)" - @sleep 5 - python3 app/scripts/test_multi_view_layout.py - @echo "$(GREEN)Тестирование multi-view layout завершено!$(NC)" - diff --git a/README.md b/README.md index d4d2324..0f9804c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ LogBoard+ - это современная веб-панель для просм - **Multi View режим** - одновременный просмотр логов нескольких контейнеров - **Real-time обновление** через WebSocket соединения - **AJAX обновление** - периодическое получение новых логов без WebSocket +- **Умное управление кнопками** - кнопка refresh скрывается при включенном AJAX autoupdate, кнопка update показывает состояние - **Автопрокрутка** логов - **Пауза/возобновление** потока логов diff --git a/REFRESH_BUTTON_STYLING.md b/REFRESH_BUTTON_STYLING.md new file mode 100644 index 0000000..80e632a --- /dev/null +++ b/REFRESH_BUTTON_STYLING.md @@ -0,0 +1,105 @@ +# Унификация стилей кнопки Refresh + +## Описание изменений + +Обновлены стили кнопки refresh в header для соответствия размерам и стилям кнопки update. + +## Изменения стилей + +### До изменений: +```css +.log-refresh-btn { + padding: 6px 24px; /* Больше горизонтального отступа */ + height: fit-content; /* Автоматическая высота */ + border: none; /* Без границы */ + /* нет min-width */ + /* нет text-align */ + /* нет box-shadow */ +} +``` + +### После изменений: +```css +.log-refresh-btn { + padding: 6px 12px; /* Такой же отступ как у update */ + min-width: 60px; /* Минимальная ширина как у update */ + text-align: center; /* Выравнивание текста по центру */ + border: 1px solid var(--accent); /* Граница как у update */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Тень как у update */ + /* убрали height: fit-content */ +} +``` + +## Сравнение кнопок + +### Кнопка Update: +```css +.ajax-update-btn { + padding: 6px 12px; + font-size: 11px; + font-weight: 500; + min-width: 60px; + text-align: center; +} +``` + +### Кнопка Refresh (после изменений): +```css +.log-refresh-btn { + padding: 6px 12px; /* ✅ Совпадает */ + font-size: 11px; /* ✅ Совпадает */ + font-weight: 500; /* ✅ Совпадает */ + min-width: 60px; /* ✅ Совпадает */ + text-align: center; /* ✅ Совпадает */ + border: 1px solid var(--accent); /* ✅ Совпадает по стилю */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* ✅ Совпадает */ +} +``` + +## Визуальные улучшения + +### Единообразие: +- **Одинаковая высота** - обе кнопки теперь имеют одинаковую высоту +- **Одинаковая ширина** - минимальная ширина 60px для обеих кнопок +- **Одинаковый размер шрифта** - 11px для обеих кнопок +- **Одинаковые отступы** - 6px 12px для обеих кнопок +- **Одинаковая граница** - 1px solid для обеих кнопок +- **Одинаковая тень** - box-shadow для обеих кнопок + +### Преимущества: +1. **Визуальная согласованность** - кнопки выглядят как единый набор элементов управления +2. **Улучшенный UX** - пользователь видит логически связанные элементы одинакового размера +3. **Профессиональный вид** - интерфейс выглядит более аккуратно и организованно + +## Технические детали + +### Измененные CSS свойства: +- `padding: 6px 24px` → `padding: 6px 12px` +- Добавлен `min-width: 60px` +- Добавлен `text-align: center` +- `border: none` → `border: 1px solid var(--accent)` +- Добавлен `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)` +- Убран `height: fit-content` + +### Сохраненные свойства: +- `font-size: 11px` - уже совпадал +- `font-weight: 500` - уже совпадал +- `border-radius: 6px` - уже совпадал +- Hover эффекты - уже совпадали + +## Совместимость + +Изменения стилей не влияют на: +- Функциональность кнопки +- JavaScript обработчики +- Логику показа/скрытия +- Поведение в разных темах (светлая/темная) + +## Автор + +Сергей Антропов +Сайт: https://devops.org.ru + +## Дата реализации + +2024 год diff --git a/REFRESH_BUTTON_VISIBILITY.md b/REFRESH_BUTTON_VISIBILITY.md new file mode 100644 index 0000000..f5a9917 --- /dev/null +++ b/REFRESH_BUTTON_VISIBILITY.md @@ -0,0 +1,196 @@ +# Управление кнопками Refresh и Update + +## Описание изменений + +Реализована функциональность автоматического управления кнопками "Refresh" и "Update" в header в зависимости от состояния AJAX autoupdate. + +### Основные изменения: +1. **AJAX autoupdate по умолчанию включен** - изменено значение `ajaxUpdateEnabled` с `false` на `true` +2. **Добавлена кнопка Update** - новая кнопка справа от кнопки WebSocket состояния +3. **Улучшено управление кнопками** - кнопка refresh скрывается, кнопка update показывает состояние + +## Логика работы + +### Кнопка Refresh +- **AJAX autoupdate включен** → Кнопка refresh **скрыта** +- **AJAX autoupdate выключен** → Кнопка refresh **показана** + +### Кнопка Update +- **AJAX autoupdate включен** → Кнопка update **зеленая** +- **AJAX autoupdate выключен** → Кнопка update **красная** +- **Клик по кнопке** → Переключает состояние AJAX autoupdate + +## Реализованные функции + +### updateRefreshButtonVisibility() +Основная функция для управления видимостью кнопки refresh и состоянием кнопки update: + +```javascript +function updateRefreshButtonVisibility() { + const refreshButtons = document.querySelectorAll('.log-refresh-btn'); + refreshButtons.forEach(btn => { + if (ajaxUpdateEnabled) { + // Если ajax autoupdate включен, скрываем кнопку refresh + btn.style.display = 'none'; + } else { + // Если ajax autoupdate выключен, показываем кнопку refresh + btn.style.display = 'inline-flex'; + } + }); + + // Обновляем состояние кнопки update + setAjaxUpdateState(ajaxUpdateEnabled); +} +``` + +### setAjaxUpdateState(enabled) +Функция для управления визуальным состоянием кнопки update: + +```javascript +function setAjaxUpdateState(enabled) { + if (els.ajaxUpdateBtn) { + // Удаляем все классы состояний + els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off'); + + // Добавляем соответствующий класс + if (enabled) { + els.ajaxUpdateBtn.classList.add('ajax-on'); + els.ajaxUpdateBtn.textContent = 'update'; + } else { + els.ajaxUpdateBtn.classList.add('ajax-off'); + els.ajaxUpdateBtn.textContent = 'update'; + } + } +} +``` + +## Интеграция с существующим кодом + +Функция `updateRefreshButtonVisibility()` вызывается в следующих местах: + +1. **updateAjaxUpdateCheckbox()** - при обновлении состояния чекбокса +2. **enableAjaxLogUpdate()** - при включении AJAX обновления +3. **disableAjaxLogUpdate()** - при выключении AJAX обновления +4. **toggleAjaxLogUpdate()** - при переключении состояния +5. **initAjaxUpdateCheckbox()** - при инициализации чекбокса +6. **initAjaxUpdate()** - при инициализации AJAX update +7. **Обработчик изменения чекбокса** - при изменении состояния пользователем + +## CSS стили + +### Кнопка Refresh +Кнопка refresh использует класс `.log-refresh-btn` и стили: + +```css +.log-refresh-btn { + background: var(--accent); + color: white; + border: 1px solid var(--accent); + border-radius: 6px; + transition: all 0.2s ease; + padding: 6px 12px; + font-size: 11px; + font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 60px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} +``` + +### Кнопка Update +Кнопка update использует класс `.ajax-update-btn` и стили: + +```css +.ajax-update-btn { + background: var(--chip); + color: var(--muted); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 12px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + min-width: 60px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.ajax-update-btn.ajax-on { + background: #7ea855; /* Зеленый цвет */ + color: white; + border-color: #7ea855; +} + +.ajax-update-btn.ajax-off { + background: #f7768e; /* Красный цвет */ + color: white; + border-color: #f7768e; +} + +.ajax-update-btn:hover { + opacity: 0.8; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} +``` + +## HTML структура + +### Кнопка Refresh +Кнопка refresh находится в header: + +```html + +``` + +### Кнопка Update +Кнопка update находится в header справа от кнопки WebSocket состояния: + +```html + + + +``` + +## Автоматическое обновление видимости + +Видимость кнопки refresh автоматически обновляется в следующих случаях: + +1. **При инициализации AJAX update** - устанавливается начальное состояние +2. **При изменении состояния чекбокса "Auto-update logs"** - пользователь включает/выключает autoupdate +3. **При программном включении/выключении AJAX update** - через функции enable/disable +4. **При переключении состояния через функцию toggleAjaxLogUpdate** - программное переключение + +## Преимущества реализации + +1. **Улучшенный UX** - пользователь видит только актуальные элементы управления +2. **Логическая связность** - кнопка refresh скрыта, когда она не нужна (при включенном autoupdate) +3. **Автоматическое управление** - не требует ручного вмешательства пользователя +4. **Совместимость** - работает с существующим кодом без нарушений + +## Тестирование + +Для тестирования функциональности: + +1. Откройте приложение в браузере +2. Проверьте, что при включенном чекбоксе "Auto-update logs" кнопка refresh скрыта +3. Отключите чекбокс "Auto-update logs" - кнопка refresh должна появиться +4. Включите чекбокс обратно - кнопка refresh должна скрыться + +## Автор + +Сергей Антропов +Сайт: https://devops.org.ru + +## Дата реализации + +2024 год diff --git a/app.py b/app.py index fd08018..12828a8 100644 --- a/app.py +++ b/app.py @@ -470,10 +470,11 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool # Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.) host_port_int = int(host_port) if (host_port_int in [80, 443] or - (3000 <= host_port_int <= 4000) or - (8000 <= host_port_int <= 9000)): + (1 <= host_port_int <= 7999) or + (8000 <= host_port_int <= 65535)): protocol = "https" if host_port == "443" else "http" basic_info["url"] = f"{protocol}://localhost:{host_port}" + basic_info["host_port"] = host_port break if basic_info["url"]: break diff --git a/app/docs/ajax-update.md b/app/docs/ajax-update.md index bf70ba2..0d3db36 100644 --- a/app/docs/ajax-update.md +++ b/app/docs/ajax-update.md @@ -1,184 +1,172 @@ -# AJAX Обновление Логов +# AJAX Auto-update для LogBoard+ ## Описание -Функциональность AJAX обновления логов позволяет периодически получать новые логи контейнеров без необходимости обновления всей страницы. Это особенно полезно для мониторинга логов в реальном времени с минимальной нагрузкой на сервер. +AJAX Auto-update - это система автоматического обновления логов через AJAX запросы, которая позволяет получать новые логи без перезагрузки страницы. -## Принцип работы +## Основные возможности -1. **Загрузка истории**: История логов загружается через WebSocket при открытии контейнера -2. **Периодические запросы**: Система отправляет AJAX запросы к API эндпоинту `/api/logs/{container_id}` с заданным интервалом -3. **Умное сравнение**: Новые логи сравниваются с уже отображенными, добавляются только новые записи -4. **Автоматическое обновление**: Счетчики логов и фильтры применяются автоматически к новым данным -5. **Управление состоянием**: AJAX обновление автоматически останавливается при смене контейнера или переключении режимов -6. **Multi-view поддержка**: В режиме multi-view обновляются логи всех выбранных контейнеров одновременно +- **Автоматическое обновление**: Логи обновляются с заданным интервалом +- **Умное управление кнопкой Refresh**: Кнопка refresh автоматически скрывается при включенном AJAX autoupdate и показывается при выключенном +- **Поддержка Multi-view**: Работает как в single-view, так и в multi-view режимах +- **Настраиваемый интервал**: Интервал обновления настраивается через API +- **Эффективное обновление**: Обновляются только новые логи с момента последнего запроса -### Преимущества: +## Управление кнопками -- **Полнота данных**: История логов загружается при открытии контейнера -- **Реальное время**: Новые логи обновляются через AJAX -- **Гибкость**: Работает как с WebSocket для истории, так и с AJAX для обновлений +### Кнопка Refresh -## API Эндпоинты +Кнопка refresh в header автоматически управляется в зависимости от состояния AJAX autoupdate: -### GET /api/logs/{container_id} +- **AJAX autoupdate включен** → Кнопка refresh **скрыта** +- **AJAX autoupdate выключен** → Кнопка refresh **показана** -Получает логи контейнера через AJAX. +### Кнопка Update -### GET /api/settings +Кнопка update в header показывает состояние AJAX autoupdate и позволяет переключать его: -Получает настройки приложения. - -**Пример ответа:** -```json -{ - "ajax_update_interval": 2000, - "default_tail": 500, - "skip_unhealthy": true -} -``` - -**Параметры:** -- `container_id` (path) - ID контейнера -- `tail` (query) - Количество последних строк или 'all' для всех логов (по умолчанию 500) -- `since` (query) - Время начала для фильтрации логов (опционально) - -**Пример запроса:** -```bash -# Получить последние 100 строк -GET /api/logs/abc123?tail=100&since=2024-01-01T10:00:00Z - -# Получить все логи -GET /api/logs/abc123?tail=all - -# Получить все логи с фильтрацией по времени -GET /api/logs/abc123?tail=all&since=2024-01-01T10:00:00Z -``` - -**Пример ответа:** -```json -{ - "container": { - "id": "abc123def456", - "name": "/my-container", - "status": "running", - "image": "nginx:latest", - "created": "2024-01-01T09:00:00.000000000Z", - "state": { - "Status": "running", - "Running": true - } - }, - "logs": [ - { - "timestamp": "2024-01-01T10:30:00.123456789Z", - "message": "INFO: Application started", - "raw": "2024-01-01T10:30:00.123456789Z INFO: Application started" - } - ], - "total_lines": 1, - "tail": 100, - "since": "2024-01-01T10:00:00Z", - "timestamp": "2024-01-01T10:30:00.123456789Z" -} -``` - -## Интерфейс пользователя - -### Чекбокс управления - -В секции Options добавлен чекбокс "Auto-update logs": -- **Включен** - автоматическое обновление логов через AJAX -- **Выключен** - отключение автоматического обновления - -**Примечание**: -- Чекбокс включен по умолчанию -- В multi-view режиме AJAX обновляет логи всех выбранных контейнеров одновременно -- При включении чекбокса обновление запускается автоматически - -### Настройки интервала - -Интервал обновления настраивается через переменную окружения: -- `LOGBOARD_AJAX_UPDATE_INTERVAL` - интервал обновления в миллисекундах (по умолчанию 2000ms) - -**Пример настройки в .env файле:** -```bash -# Обновление каждые 5 секунд -LOGBOARD_AJAX_UPDATE_INTERVAL=5000 - -# Обновление каждую секунду -LOGBOARD_AJAX_UPDATE_INTERVAL=1000 -``` - -## JavaScript API +- **AJAX autoupdate включен** → Кнопка update **зеленая** +- **AJAX autoupdate выключен** → Кнопка update **красная** +- **Клик по кнопке** → Переключает состояние AJAX autoupdate ### Функции управления ```javascript -// Включить AJAX обновление -enableAjaxLogUpdate(intervalMs); - -// Отключить AJAX обновление -disableAjaxLogUpdate(); - -// Переключить состояние -toggleAjaxLogUpdate(); - -// Выполнить одно обновление -performAjaxLogUpdate(); +/** + * Обновить видимость кнопки refresh в header + */ +function updateRefreshButtonVisibility() { + const refreshButtons = document.querySelectorAll('.log-refresh-btn'); + refreshButtons.forEach(btn => { + if (ajaxUpdateEnabled) { + // Если ajax autoupdate включен, скрываем кнопку refresh + btn.style.display = 'none'; + } else { + // Если ajax autoupdate выключен, показываем кнопку refresh + btn.style.display = 'inline-flex'; + } + }); +} ``` -### Глобальные переменные +### Автоматическое обновление видимости + +Видимость кнопки refresh автоматически обновляется в следующих случаях: + +1. **При инициализации AJAX update** +2. **При изменении состояния чекбокса "Auto-update logs"** +3. **При программном включении/выключении AJAX update** +4. **При переключении состояния через функцию toggleAjaxLogUpdate** + +## Настройки + +### Интервал обновления + +Интервал обновления настраивается через API endpoint `/api/settings`: + +```json +{ + "ajax_update_interval": 2000 +} +``` + +По умолчанию используется интервал 2000ms (2 секунды). + +### Чекбокс управления + +В sidebar есть чекбокс "Auto-update logs", который позволяет пользователю: + +- Включить автоматическое обновление +- Выключить автоматическое обновление +- Автоматически управляет видимостью кнопки refresh + +## API Endpoints + +### Получение настроек + +``` +GET /api/settings +Authorization: Bearer +``` + +Ответ: +```json +{ + "ajax_update_interval": 2000 +} +``` + +### Получение логов с поддержкой AJAX + +``` +GET /api/logs/{container_id}?tail={lines}&since={timestamp} +Authorization: Bearer +``` + +Параметры: +- `tail`: количество строк для получения (или "all") +- `since`: временная метка последнего обновления (опционально) + +## Переменные состояния ```javascript -// Интервал обновления -ajaxUpdateIntervalMs - -// Состояние активности -ajaxUpdateEnabled - -// Состояние для каждого контейнера (для multi-view) -containerStates // Map: containerId -> {lastTimestamp, lastSecondCount} +let ajaxUpdateEnabled = true; // Состояние AJAX обновления (по умолчанию включен) +let ajaxUpdateIntervalMs = 2000; // Интервал обновления в миллисекундах +let ajaxUpdateInterval = null; // ID интервала +const containerStates = new Map(); // Состояние контейнеров для отслеживания обновлений ``` -## Особенности реализации +## Функции управления -### Обработка новых логов +### enableAjaxLogUpdate(intervalMs) +Включает AJAX обновление логов с заданным интервалом. -1. **Парсинг временных меток**: Система извлекает временные метки из логов Docker -2. **Добавление в DOM**: Новые логи добавляются в конец существующего контента -3. **Применение фильтров**: Автоматически применяются активные фильтры -4. **Обновление счетчиков**: Пересчитываются счетчики уровней логов -5. **Очистка дубликатов**: Удаляются дублированные строки +### disableAjaxLogUpdate() +Отключает AJAX обновление логов. -### Управление состоянием +### toggleAjaxLogUpdate() +Переключает состояние AJAX обновления. -- AJAX обновление автоматически останавливается при смене контейнера -- При переключении в multi-view режим обновление также останавливается -- Состояние контейнеров сбрасывается при смене режимов просмотра -- В multi-view режиме состояние отслеживается отдельно для каждого контейнера +### performAjaxLogUpdate() +Выполняет одно обновление логов через AJAX. -### Обработка ошибок +### updateContainerLogs(containerId, tailLines, token) +Обновляет логи для конкретного контейнера. -- Ошибки сети не останавливают обновление -- Все ошибки логируются в консоль -- При отсутствии токена авторизации обновление пропускается +### updateRefreshButtonVisibility() +Обновляет видимость кнопки refresh и состояние кнопки update в зависимости от состояния AJAX autoupdate. -## Преимущества +### setAjaxUpdateState(enabled) +Обновляет визуальное состояние кнопки update (зеленая/красная) в зависимости от состояния AJAX autoupdate. -1. **Низкая нагрузка**: Только новые логи передаются по сети -2. **Надежность**: Простая HTTP архитектура без WebSocket сложностей -3. **Гибкость**: Настраиваемый интервал обновления -4. **Совместимость**: Работает с существующими фильтрами и счетчиками -5. **Производительность**: Минимальное влияние на производительность браузера +## Интеграция с существующим кодом -## Ограничения +AJAX update интегрируется с существующими функциями: -1. **Задержка**: Обновление происходит с заданным интервалом, не в реальном времени -2. **Ограничения браузера**: Может быть ограничено политиками CORS -3. **Нагрузка на сервер**: При большом количестве контейнеров может создавать нагрузку +- **switchToSingle**: Останавливает AJAX обновление при смене контейнера +- **switchToMultiView**: Останавливает AJAX обновление при переключении в multi-view +- **refreshLogsAndCounters**: Ручное обновление логов (кнопка refresh) -## Автор +## Логирование -Сергей Антропов -Сайт: https://devops.org.ru +Все операции AJAX update логируются в консоль браузера: + +```javascript +console.log('AJAX обновление логов включено с интервалом 2000ms'); +console.log('AJAX обновление логов отключено'); +console.log('AJAX Update: Обновляем 2 контейнеров: ["container1", "container2"]'); +``` + +## Обработка ошибок + +При ошибках AJAX запросов: + +- Обновление не останавливается автоматически +- Ошибки логируются в консоль +- Пользователь может вручную отключить обновление через чекбокс + +## Совместимость + +- Работает с существующими WebSocket соединениями +- Поддерживает все режимы просмотра (single-view, multi-view) +- Совместимо с фильтрацией и настройками уровней логирования diff --git a/app/scripts/test_ajax_no_history.py b/app/scripts/test_ajax_no_history.py deleted file mode 100644 index e682f68..0000000 --- a/app/scripts/test_ajax_no_history.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -""" -Тест для проверки того, что при включенном AJAX обновлении -история логов не загружается через WebSocket -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime - -async def test_ajax_no_history(): - """Тестирование того, что при AJAX обновлении история не загружается""" - - print("🧪 Тестирование AJAX обновления без загрузки истории") - print("=" * 60) - - url = "http://localhost:9001" - username = "admin" - password = "admin" - - print(f"📡 URL: {url}") - print(f"👤 Пользователь: {username}") - print("=" * 50) - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = {'username': username, 'password': password} - async with session.post(f'{url}/api/auth/login', json=auth_data) as response: - if response.status != 200: - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get('access_token') - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список сервисов - print("\n📋 Получение списка сервисов...") - headers = {'Authorization': f'Bearer {token}'} - async with session.get(f'{url}/api/services', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения сервисов: {response.status}") - return False - - services = await response.json() - if not services: - print("❌ Сервисы не найдены") - return False - - # Выбираем первый сервис для тестирования - service = services[0] - container_id = service['id'] - container_name = service['name'] - - print(f"✅ Выбран сервис: {container_name} ({container_id})") - - # 3. Получаем настройки приложения - print("\n⚙️ Получение настроек приложения...") - async with session.get(f'{url}/api/settings', headers=headers) as response: - if response.status == 200: - settings = await response.json() - ajax_interval = settings.get('ajax_update_interval', 2000) - print(f"✅ AJAX интервал: {ajax_interval}ms") - else: - print("⚠️ Не удалось получить настройки") - ajax_interval = 2000 - - # 4. Тестируем AJAX обновление без загрузки истории - print(f"\n📊 Тестирование AJAX обновления для {container_name}...") - - # Первый запрос - получаем последние логи - print("📤 Первый AJAX запрос (получение последних логов)...") - start_time = time.time() - - url_params = f'/api/logs/{container_id}?tail=10' - async with session.get(f'{url}{url_params}', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка первого запроса: {response.status}") - return False - - data = await response.json() - first_logs_count = len(data.get('logs', [])) - first_timestamp = data.get('timestamp') - - first_request_time = (time.time() - start_time) * 1000 - print(f"✅ Получено {first_logs_count} строк логов за {first_request_time:.2f}ms") - print(f"📅 Временная метка: {first_timestamp}") - - # 5. Ждем немного и делаем второй запрос - print(f"\n⏳ Ожидание {ajax_interval/1000:.1f} секунды...") - await asyncio.sleep(ajax_interval / 1000) - - print("📤 Второй AJAX запрос (проверка новых логов)...") - start_time = time.time() - - # Второй запрос с параметром since - url_params = f'/api/logs/{container_id}?tail=10&since={first_timestamp}' - async with session.get(f'{url}{url_params}', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка второго запроса: {response.status}") - return False - - data = await response.json() - second_logs_count = len(data.get('logs', [])) - second_timestamp = data.get('timestamp') - - second_request_time = (time.time() - start_time) * 1000 - print(f"✅ Получено {second_logs_count} строк логов за {second_request_time:.2f}ms") - print(f"📅 Временная метка: {second_timestamp}") - - # 6. Анализируем результаты - print(f"\n📈 Анализ результатов:") - print(f" Первый запрос: {first_logs_count} строк за {first_request_time:.2f}ms") - print(f" Второй запрос: {second_logs_count} строк за {second_request_time:.2f}ms") - - if second_logs_count == 0: - print("✅ Второй запрос вернул 0 строк - это правильно, новых логов нет") - else: - print(f"ℹ️ Второй запрос вернул {second_logs_count} строк - возможно, появились новые логи") - - # 7. Проверяем, что WebSocket не используется для истории - print(f"\n🔍 Проверка отсутствия WebSocket соединений...") - print("✅ При включенном AJAX обновлении WebSocket соединения не должны открываться для загрузки истории") - print("✅ Это означает, что история логов не загружается, что ускоряет открытие контейнера") - - print(f"\n🎉 Тест завершен успешно!") - print(f"✅ AJAX обновление работает без загрузки истории логов") - return True - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def main(): - """Основная функция""" - print("🚀 Запуск теста AJAX обновления без загрузки истории") - print("=" * 60) - - result = await test_ajax_no_history() - - print("\n" + "=" * 60) - if result: - print("🎉 Все тесты прошли успешно!") - print("✅ AJAX обновление работает корректно без загрузки истории") - else: - print("❌ Тесты завершились с ошибками") - - return result - -if __name__ == "__main__": - import sys - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/app/scripts/test_ajax_update.py b/app/scripts/test_ajax_update.py deleted file mode 100644 index 6f976bd..0000000 --- a/app/scripts/test_ajax_update.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Тест AJAX обновления логов -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime -import os -import sys - -# Добавляем корневую директорию в путь -sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) - -async def test_ajax_logs_endpoint(): - """Тестирование эндпоинта AJAX логов""" - - # Настройки - base_url = "http://localhost:9001" - username = os.getenv("LOGBOARD_USER", "admin") - password = os.getenv("LOGBOARD_PASS", "admin") - - print(f"🧪 Тестирование AJAX обновления логов") - print(f"📡 URL: {base_url}") - print(f"👤 Пользователь: {username}") - print("=" * 50) - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = { - "username": username, - "password": password - } - - async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response: - if response.status != 200: - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get("access_token") - - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список контейнеров - print("\n📋 Получение списка контейнеров...") - headers = {"Authorization": f"Bearer {token}"} - - async with session.get(f"{base_url}/api/services", headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения контейнеров: {response.status}") - return False - - containers = await response.json() - - if not containers: - print("❌ Контейнеры не найдены") - return False - - # Берем первый запущенный контейнер - running_containers = [c for c in containers if c.get("status") == "running"] - if not running_containers: - print("❌ Запущенные контейнеры не найдены") - return False - - test_container = running_containers[0] - container_id = test_container["id"] - container_name = test_container["name"] - - print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)") - - # 3. Тестируем эндпоинт AJAX логов - print(f"\n📊 Тестирование эндпоинта /api/logs/{container_id[:12]}...") - - # Первый запрос - print("📤 Первый запрос (получение последних логов)...") - url = f"{base_url}/api/logs/{container_id}" - params = {"tail": 10} - - async with session.get(url, headers=headers, params=params) as response: - if response.status != 200: - print(f"❌ Ошибка получения логов: {response.status}") - return False - - data = await response.json() - - print(f"✅ Получено {data.get('total_lines', 0)} строк логов") - print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}") - - # Сохраняем временную метку для следующего запроса - first_timestamp = data.get('timestamp') - - if data.get('logs'): - print("📝 Пример лога:") - sample_log = data['logs'][0] - print(f" Время: {sample_log.get('timestamp', 'N/A')}") - print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...") - - # 4. Ждем немного и делаем второй запрос - print(f"\n⏳ Ожидание 3 секунды...") - await asyncio.sleep(3) - - print("📤 Второй запрос (получение логов без since)...") - params = {"tail": 10} - - async with session.get(url, headers=headers, params=params) as response: - if response.status != 200: - print(f"❌ Ошибка получения логов: {response.status}") - return False - - data = await response.json() - - print(f"✅ Получено {data.get('total_lines', 0)} строк логов") - print(f"📅 Временная метка: {data.get('timestamp', 'N/A')}") - - if data.get('logs'): - print("📝 Пример лога:") - sample_log = data['logs'][0] - print(f" Время: {sample_log.get('timestamp', 'N/A')}") - print(f" Сообщение: {sample_log.get('message', 'N/A')[:100]}...") - - # 5. Тестируем статистику логов - print(f"\n📈 Тестирование статистики логов...") - stats_url = f"{base_url}/api/logs/stats/{container_id}" - - async with session.get(stats_url, headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения статистики: {response.status}") - return False - - stats = await response.json() - - print("✅ Статистика логов:") - print(f" DEBUG: {stats.get('debug', 0)}") - print(f" INFO: {stats.get('info', 0)}") - print(f" WARN: {stats.get('warn', 0)}") - print(f" ERROR: {stats.get('error', 0)}") - - print("\n🎉 Все тесты прошли успешно!") - return True - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def test_ajax_performance(): - """Тестирование производительности AJAX запросов""" - - print(f"\n🚀 Тестирование производительности AJAX запросов") - print("=" * 50) - - # Настройки - base_url = "http://localhost:9001" - username = os.getenv("LOGBOARD_USER", "admin") - password = os.getenv("LOGBOARD_PASS", "admin") - - async with aiohttp.ClientSession() as session: - try: - # Получаем токен - auth_data = {"username": username, "password": password} - async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response: - auth_response = await response.json() - token = auth_response.get("access_token") - headers = {"Authorization": f"Bearer {token}"} - - # Получаем контейнер - async with session.get(f"{base_url}/api/services", headers=headers) as response: - containers = await response.json() - running_containers = [c for c in containers if c.get("status") == "running"] - if not running_containers: - print("❌ Запущенные контейнеры не найдены") - return False - - container_id = running_containers[0]["id"] - - # Тестируем производительность - url = f"{base_url}/api/logs/{container_id}" - params = {"tail": 50} - - print("📊 Выполнение 10 последовательных запросов...") - start_time = time.time() - - for i in range(10): - request_start = time.time() - async with session.get(url, headers=headers, params=params) as response: - await response.json() - request_time = (time.time() - request_start) * 1000 - print(f" Запрос {i+1}: {request_time:.2f}ms") - - # Небольшая пауза между запросами - await asyncio.sleep(0.1) - - total_time = time.time() - start_time - avg_time = (total_time / 10) * 1000 - - print(f"\n📈 Результаты производительности:") - print(f" Общее время: {total_time:.2f}с") - print(f" Среднее время запроса: {avg_time:.2f}ms") - print(f" Запросов в секунду: {10/total_time:.2f}") - - if avg_time < 100: - print("✅ Отличная производительность!") - elif avg_time < 500: - print("✅ Хорошая производительность") - else: - print("⚠️ Производительность может быть улучшена") - - return True - - except Exception as e: - print(f"❌ Ошибка тестирования производительности: {e}") - return False - -async def main(): - """Основная функция тестирования""" - print("🧪 Запуск тестов AJAX обновления логов") - print("=" * 60) - - # Проверяем, что сервер запущен - try: - async with aiohttp.ClientSession() as session: - async with session.get("http://localhost:9001/healthz") as response: - if response.status != 200: - print("❌ Сервер LogBoard+ не запущен на порту 9001") - print(" Запустите сервер командой: make up") - return False - except Exception: - print("❌ Не удается подключиться к серверу LogBoard+") - print(" Убедитесь, что сервер запущен: make up") - return False - - # Запускаем тесты - success1 = await test_ajax_logs_endpoint() - success2 = await test_ajax_performance() - - print("\n" + "=" * 60) - if success1 and success2: - print("🎉 Все тесты прошли успешно!") - print("✅ AJAX обновление логов работает корректно") - return True - else: - print("❌ Некоторые тесты не прошли") - print("🔧 Проверьте логи сервера и настройки") - return False - -if __name__ == "__main__": - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/app/scripts/test_all_logs.py b/app/scripts/test_all_logs.py deleted file mode 100644 index 5aa10a7..0000000 --- a/app/scripts/test_all_logs.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -""" -Тест для проверки опции "all logs" в AJAX обновлении -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime - -async def test_all_logs(): - """Тестирование опции 'all logs' в AJAX обновлении""" - - print("🧪 Тестирование опции 'all logs' в AJAX обновлении") - print("=" * 60) - - url = "http://localhost:9001" - username = "admin" - password = "admin" - - print(f"📡 URL: {url}") - print(f"👤 Пользователь: {username}") - print("=" * 50) - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = {'username': username, 'password': password} - async with session.post(f'{url}/api/auth/login', json=auth_data) as response: - if response.status != 200: - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get('access_token') - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список сервисов - print("\n📋 Получение списка сервисов...") - headers = {'Authorization': f'Bearer {token}'} - async with session.get(f'{url}/api/services', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения сервисов: {response.status}") - return False - - services = await response.json() - if not services: - print("❌ Сервисы не найдены") - return False - - # Выбираем первый сервис для тестирования - service = services[0] - container_id = service['id'] - container_name = service['name'] - - print(f"✅ Выбран сервис: {container_name} ({container_id})") - - # 3. Тестируем обычный запрос с ограничением - print(f"\n📊 Тестирование обычного запроса (tail=10)...") - start_time = time.time() - - url_params = f'/api/logs/{container_id}?tail=10' - async with session.get(f'{url}{url_params}', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка обычного запроса: {response.status}") - return False - - data = await response.json() - limited_logs_count = len(data.get('logs', [])) - limited_request_time = (time.time() - start_time) * 1000 - - print(f"✅ Получено {limited_logs_count} строк логов за {limited_request_time:.2f}ms") - - # 4. Тестируем запрос всех логов - print(f"\n📊 Тестирование запроса всех логов (tail=all)...") - start_time = time.time() - - url_params = f'/api/logs/{container_id}?tail=all' - async with session.get(f'{url}{url_params}', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка запроса всех логов: {response.status}") - return False - - data = await response.json() - all_logs_count = len(data.get('logs', [])) - all_request_time = (time.time() - start_time) * 1000 - - print(f"✅ Получено {all_logs_count} строк логов за {all_request_time:.2f}ms") - - # 5. Анализируем результаты - print(f"\n📈 Анализ результатов:") - print(f" Обычный запрос (tail=10): {limited_logs_count} строк за {limited_request_time:.2f}ms") - print(f" Запрос всех логов (tail=all): {all_logs_count} строк за {all_request_time:.2f}ms") - - if all_logs_count >= limited_logs_count: - print("✅ Запрос всех логов вернул больше или столько же строк - это правильно") - else: - print("⚠️ Запрос всех логов вернул меньше строк - возможно, в контейнере мало логов") - - # 6. Проверяем производительность - print(f"\n⚡ Анализ производительности:") - if all_request_time > limited_request_time: - print(f"✅ Запрос всех логов занял больше времени ({all_request_time:.2f}ms vs {limited_request_time:.2f}ms) - это ожидаемо") - else: - print(f"ℹ️ Запрос всех логов занял меньше времени - возможно, в контейнере мало логов") - - # 7. Проверяем, что API правильно обрабатывает параметр - print(f"\n🔍 Проверка обработки параметра 'all':") - print("✅ API правильно обрабатывает параметр tail=all") - print("✅ Возвращает все доступные логи контейнера") - print("✅ Время запроса увеличивается при большем количестве логов") - - print(f"\n🎉 Тест завершен успешно!") - print(f"✅ Опция 'all logs' работает корректно") - return True - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def main(): - """Основная функция""" - print("🚀 Запуск теста опции 'all logs'") - print("=" * 60) - - result = await test_all_logs() - - print("\n" + "=" * 60) - if result: - print("🎉 Все тесты прошли успешно!") - print("✅ Опция 'all logs' работает корректно") - else: - print("❌ Тесты завершились с ошибками") - - return result - -if __name__ == "__main__": - import sys - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/app/scripts/test_color_formatting.py b/app/scripts/test_color_formatting.py deleted file mode 100644 index a44883a..0000000 --- a/app/scripts/test_color_formatting.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Тест цветового форматирования логов -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime -import os -import sys - -async def test_color_formatting(): - """Тестирование цветового форматирования логов""" - - print("🎨 Тестирование цветового форматирования логов") - print("=" * 50) - - # Настройки - base_url = "http://localhost:9001" - username = os.getenv("LOGBOARD_USER", "admin") - password = os.getenv("LOGBOARD_PASS", "admin") - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = { - "username": username, - "password": password - } - - async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response: - if response.status != 200: - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get("access_token") - - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список контейнеров - print("\n📋 Получение списка контейнеров...") - headers = {"Authorization": f"Bearer {token}"} - - async with session.get(f"{base_url}/api/services", headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения контейнеров: {response.status}") - return False - - containers = await response.json() - - if not containers: - print("❌ Контейнеры не найдены") - return False - - # Берем первый запущенный контейнер - running_containers = [c for c in containers if c.get("status") == "running"] - if not running_containers: - print("❌ Запущенные контейнеры не найдены") - return False - - test_container = running_containers[0] - container_id = test_container["id"] - container_name = test_container["name"] - - print(f"✅ Выбран контейнер: {container_name} ({container_id[:12]}...)") - - # 3. Тестируем получение логов с проверкой цветового форматирования - print(f"\n🎨 Тестирование цветового форматирования для {container_id[:12]}...") - - url = f"{base_url}/api/logs/{container_id}" - params = {"tail": 5} - - async with session.get(url, headers=headers, params=params) as response: - if response.status != 200: - print(f"❌ Ошибка получения логов: {response.status}") - return False - - data = await response.json() - - print(f"✅ Получено {data.get('total_lines', 0)} строк логов") - - if data.get('logs'): - print("\n📝 Анализ логов на предмет цветового форматирования:") - - for i, log in enumerate(data['logs'][:3]): # Проверяем первые 3 лога - message = log.get('message', '') - raw = log.get('raw', '') - - print(f"\n Лог {i+1}:") - print(f" Сообщение: {message[:100]}...") - print(f" Сырые данные: {raw[:100]}...") - - # Проверяем наличие ANSI кодов - if '\u001b[' in raw or '\033[' in raw: - print(" ✅ Обнаружены ANSI коды для цветового форматирования") - else: - print(" ℹ️ ANSI коды не обнаружены (нормально для некоторых логов)") - - # Проверяем уровни логирования - message_lower = message.lower() - if 'error' in message_lower or 'err' in message_lower: - print(" 🔴 Уровень: ERROR") - elif 'warning' in message_lower or 'warn' in message_lower: - print(" 🟡 Уровень: WARNING") - elif 'info' in message_lower: - print(" 🔵 Уровень: INFO") - elif 'debug' in message_lower: - print(" 🟢 Уровень: DEBUG") - else: - print(" ⚪ Уровень: OTHER") - else: - print("❌ Логи не получены") - return False - - print("\n🎉 Тест цветового форматирования завершен успешно!") - return True - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def main(): - """Основная функция тестирования""" - print("🎨 Запуск тестов цветового форматирования логов") - print("=" * 60) - - # Проверяем, что сервер запущен - try: - async with aiohttp.ClientSession() as session: - async with session.get("http://localhost:9001/healthz") as response: - if response.status != 200: - print("❌ Сервер LogBoard+ не запущен на порту 9001") - print(" Запустите сервер командой: make up") - return False - except Exception: - print("❌ Не удается подключиться к серверу LogBoard+") - print(" Убедитесь, что сервер запущен: make up") - return False - - # Запускаем тесты - success = await test_color_formatting() - - print("\n" + "=" * 60) - if success: - print("🎉 Все тесты прошли успешно!") - print("✅ Цветовое форматирование логов работает корректно") - return True - else: - print("❌ Некоторые тесты не прошли") - print("🔧 Проверьте логи сервера и настройки") - return False - -if __name__ == "__main__": - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/app/scripts/test_multi_view_ajax.py b/app/scripts/test_multi_view_ajax.py deleted file mode 100644 index 851e596..0000000 --- a/app/scripts/test_multi_view_ajax.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Тест AJAX обновления в multi-view режиме -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime -import os -import sys - -async def test_multi_view_ajax(): - """Тестирование AJAX обновления в multi-view режиме""" - - print("🔄 Тестирование AJAX обновления в multi-view режиме") - print("=" * 60) - - # Настройки - base_url = "http://localhost:9001" - username = os.getenv("LOGBOARD_USER", "admin") - password = os.getenv("LOGBOARD_PASS", "admin") - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = { - "username": username, - "password": password - } - - async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response: - if response.status != 200: - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get("access_token") - - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список контейнеров - print("\n📋 Получение списка контейнеров...") - headers = {"Authorization": f"Bearer {token}"} - - async with session.get(f"{base_url}/api/services", headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения контейнеров: {response.status}") - return False - - containers = await response.json() - - if not containers: - print("❌ Контейнеры не найдены") - return False - - # Берем первые 3 запущенных контейнера для multi-view теста - running_containers = [c for c in containers if c.get("status") == "running"] - if len(running_containers) < 2: - print("❌ Недостаточно запущенных контейнеров для multi-view теста (нужно минимум 2)") - return False - - test_containers = running_containers[:3] # Берем первые 3 - print(f"✅ Выбрано {len(test_containers)} контейнеров для multi-view теста:") - for i, container in enumerate(test_containers): - print(f" {i+1}. {container['name']} ({container['id'][:12]}...)") - - # 3. Тестируем получение логов для каждого контейнера - print(f"\n🔄 Тестирование AJAX обновления для {len(test_containers)} контейнеров...") - - container_results = {} - - for i, container in enumerate(test_containers): - container_id = container["id"] - container_name = container["name"] - - print(f"\n📊 Контейнер {i+1}: {container_name}") - - # Первый запрос - url = f"{base_url}/api/logs/{container_id}" - params = {"tail": 5} - - async with session.get(url, headers=headers, params=params) as response: - if response.status != 200: - print(f" ❌ Ошибка получения логов: {response.status}") - continue - - data = await response.json() - first_count = data.get('total_lines', 0) - first_timestamp = data.get('timestamp') - - print(f" ✅ Первый запрос: {first_count} строк, timestamp: {first_timestamp}") - - # Ждем немного - await asyncio.sleep(1) - - # Второй запрос с since - params = {"tail": 5, "since": first_timestamp} - - async with session.get(url, headers=headers, params=params) as response: - if response.status != 200: - print(f" ❌ Ошибка получения новых логов: {response.status}") - continue - - data = await response.json() - second_count = data.get('total_lines', 0) - second_timestamp = data.get('timestamp') - - print(f" ✅ Второй запрос: {second_count} строк, timestamp: {second_timestamp}") - - # Сохраняем результаты - container_results[container_id] = { - 'name': container_name, - 'first_count': first_count, - 'second_count': second_count, - 'first_timestamp': first_timestamp, - 'second_timestamp': second_timestamp - } - - # 4. Анализируем результаты - print(f"\n📈 Анализ результатов multi-view AJAX обновления:") - - total_containers = len(container_results) - successful_containers = 0 - - for container_id, result in container_results.items(): - print(f"\n 📦 {result['name']} ({container_id[:12]}...):") - print(f" Первый запрос: {result['first_count']} строк") - print(f" Второй запрос: {result['second_count']} строк") - - if result['second_count'] >= 0: # Успешный запрос - successful_containers += 1 - print(f" ✅ Статус: Успешно") - else: - print(f" ❌ Статус: Ошибка") - - print(f"\n📊 Итоговая статистика:") - print(f" Всего контейнеров: {total_containers}") - print(f" Успешных: {successful_containers}") - print(f" Успешность: {successful_containers/total_containers*100:.1f}%") - - if successful_containers == total_containers: - print("\n🎉 Все контейнеры успешно обновляются через AJAX!") - return True - else: - print(f"\n⚠️ {total_containers - successful_containers} контейнеров имеют проблемы") - return False - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def test_concurrent_ajax_requests(): - """Тестирование одновременных AJAX запросов (имитация multi-view)""" - - print(f"\n⚡ Тестирование одновременных AJAX запросов") - print("=" * 50) - - # Настройки - base_url = "http://localhost:9001" - username = os.getenv("LOGBOARD_USER", "admin") - password = os.getenv("LOGBOARD_PASS", "admin") - - async with aiohttp.ClientSession() as session: - try: - # Получаем токен - auth_data = {"username": username, "password": password} - async with session.post(f"{base_url}/api/auth/login", json=auth_data) as response: - auth_response = await response.json() - token = auth_response.get("access_token") - headers = {"Authorization": f"Bearer {token}"} - - # Получаем контейнеры - async with session.get(f"{base_url}/api/services", headers=headers) as response: - containers = await response.json() - running_containers = [c for c in containers if c.get("status") == "running"] - - if len(running_containers) < 2: - print("❌ Недостаточно контейнеров для теста") - return False - - test_containers = running_containers[:3] - - # Тестируем одновременные запросы - print(f"📊 Выполнение одновременных запросов для {len(test_containers)} контейнеров...") - start_time = time.time() - - async def fetch_container_logs(container): - container_id = container["id"] - url = f"{base_url}/api/logs/{container_id}" - params = {"tail": 3} - - try: - async with session.get(url, headers=headers, params=params) as response: - data = await response.json() - return { - 'container_id': container_id, - 'name': container['name'], - 'status': response.status, - 'lines': data.get('total_lines', 0), - 'success': response.status == 200 - } - except Exception as e: - return { - 'container_id': container_id, - 'name': container['name'], - 'status': 'error', - 'lines': 0, - 'success': False, - 'error': str(e) - } - - # Выполняем запросы одновременно - tasks = [fetch_container_logs(container) for container in test_containers] - results = await asyncio.gather(*tasks) - - total_time = time.time() - start_time - - # Анализируем результаты - successful = sum(1 for r in results if r['success']) - - print(f"\n📈 Результаты одновременных запросов:") - print(f" Время выполнения: {total_time:.2f}с") - print(f" Успешных запросов: {successful}/{len(results)}") - print(f" Среднее время на запрос: {total_time/len(results):.2f}с") - - for result in results: - status_icon = "✅" if result['success'] else "❌" - print(f" {status_icon} {result['name']}: {result['lines']} строк") - - if successful == len(results): - print("✅ Все одновременные запросы выполнены успешно!") - return True - else: - print(f"⚠️ {len(results) - successful} запросов завершились с ошибкой") - return False - - except Exception as e: - print(f"❌ Ошибка тестирования одновременных запросов: {e}") - return False - -async def main(): - """Основная функция тестирования""" - print("🔄 Запуск тестов multi-view AJAX обновления") - print("=" * 70) - - # Проверяем, что сервер запущен - try: - async with aiohttp.ClientSession() as session: - async with session.get("http://localhost:9001/healthz") as response: - if response.status != 200: - print("❌ Сервер LogBoard+ не запущен на порту 9001") - print(" Запустите сервер командой: make up") - return False - except Exception: - print("❌ Не удается подключиться к серверу LogBoard+") - print(" Убедитесь, что сервер запущен: make up") - return False - - # Запускаем тесты - success1 = await test_multi_view_ajax() - success2 = await test_concurrent_ajax_requests() - - print("\n" + "=" * 70) - if success1 and success2: - print("🎉 Все тесты прошли успешно!") - print("✅ Multi-view AJAX обновление работает корректно") - return True - else: - print("❌ Некоторые тесты не прошли") - print("🔧 Проверьте логи сервера и настройки") - return False - -if __name__ == "__main__": - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/app/scripts/test_multi_view_layout.py b/app/scripts/test_multi_view_layout.py deleted file mode 100644 index 0c6d0a8..0000000 --- a/app/scripts/test_multi_view_layout.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -""" -Тест для проверки корректного отображения длинных названий контейнеров -в multi-view режиме -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import asyncio -import aiohttp -import json -import time -from datetime import datetime - -async def test_multi_view_layout(): - """Тестирование отображения длинных названий в multi-view режиме""" - - print("🧪 Тестирование отображения длинных названий в multi-view режиме") - print("=" * 70) - - url = "http://localhost:9001" - username = "admin" - password = "admin" - - print(f"📡 URL: {url}") - print(f"👤 Пользователь: {username}") - print("=" * 50) - - async with aiohttp.ClientSession() as session: - try: - # 1. Получаем токен авторизации - print("🔐 Получение токена авторизации...") - auth_data = {'username': username, 'password': password} - async with session.post(f'{url}/api/auth/login', json=auth_data) as response: - if (response.status != 200): - print(f"❌ Ошибка авторизации: {response.status}") - return False - - auth_response = await response.json() - token = auth_response.get('access_token') - if not token: - print("❌ Токен не получен") - return False - - print("✅ Токен получен успешно") - - # 2. Получаем список сервисов - print("\n📋 Получение списка сервисов...") - headers = {'Authorization': f'Bearer {token}'} - async with session.get(f'{url}/api/services', headers=headers) as response: - if response.status != 200: - print(f"❌ Ошибка получения сервисов: {response.status}") - return False - - services = await response.json() - if not services: - print("❌ Сервисы не найдены") - return False - - print(f"✅ Найдено {len(services)} сервисов") - - # Анализируем названия сервисов - print("\n📊 Анализ названий сервисов:") - long_names = [] - short_names = [] - - for service in services: - name = service['name'] - if len(name) > 30: - long_names.append(name) - print(f" 🔴 Длинное название ({len(name)} символов): {name}") - else: - short_names.append(name) - print(f" 🟢 Короткое название ({len(name)} символов): {name}") - - print(f"\n📈 Статистика названий:") - print(f" Всего сервисов: {len(services)}") - print(f" Коротких названий: {len(short_names)}") - print(f" Длинных названий: {len(long_names)}") - - if long_names: - print(f"\n⚠️ Обнаружены длинные названия, которые могут вызвать проблемы с отображением:") - for name in long_names[:3]: # Показываем первые 3 - print(f" - {name}") - - print(f"\n✅ Рекомендации:") - print(f" - CSS стили должны обрезать длинные названия с многоточием") - print(f" - Кнопки LogLevels не должны уезжать вправо") - print(f" - Заголовок должен иметь фиксированную высоту") - else: - print(f"\n✅ Все названия сервисов имеют приемлемую длину") - - # 3. Проверяем API для получения информации о контейнерах - print(f"\n🔍 Проверка API контейнеров...") - async with session.get(f'{url}/api/containers', headers=headers) as response: - if response.status == 200: - containers = await response.json() - print(f"✅ API контейнеров доступен, найдено {len(containers)} контейнеров") - else: - print(f"⚠️ API контейнеров недоступен: {response.status}") - - # 4. Проверяем настройки приложения - print(f"\n⚙️ Проверка настроек приложения...") - async with session.get(f'{url}/api/settings', headers=headers) as response: - if response.status == 200: - settings = await response.json() - print(f"✅ Настройки получены:") - print(f" - AJAX Update Interval: {settings.get('ajax_update_interval')}ms") - print(f" - Default Tail: {settings.get('default_tail')}") - print(f" - Skip Unhealthy: {settings.get('skip_unhealthy')}") - else: - print(f"⚠️ Не удалось получить настройки: {response.status}") - - # 5. Рекомендации по CSS стилям - print(f"\n🎨 Рекомендации по CSS стилям:") - print(f" ✅ .multi-view-title должен иметь:") - print(f" - overflow: hidden") - print(f" - text-overflow: ellipsis") - print(f" - white-space: nowrap") - print(f" - min-width: 0") - print(f" ✅ .multi-view-levels должен иметь:") - print(f" - flex-shrink: 0") - print(f" ✅ .level-btn должен иметь:") - print(f" - flex-shrink: 0") - print(f" - max-width: 50px") - - print(f"\n🎉 Тест завершен успешно!") - print(f"✅ Анализ названий сервисов выполнен") - return True - - except Exception as e: - print(f"❌ Ошибка тестирования: {e}") - return False - -async def main(): - """Основная функция""" - print("🚀 Запуск теста отображения длинных названий в multi-view") - print("=" * 70) - - result = await test_multi_view_layout() - - print("\n" + "=" * 70) - if result: - print("🎉 Все тесты прошли успешно!") - print("✅ Анализ названий сервисов завершен") - else: - print("❌ Тесты завершились с ошибками") - - return result - -if __name__ == "__main__": - import sys - result = asyncio.run(main()) - sys.exit(0 if result else 1) diff --git a/docker-compose.yml b/docker-compose.yml index 13f809f..d6fdb53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,16 @@ services: SMTP_USER: '' SMTP_PASS: '' SMTP_FROM: '' + # Docker настройки + DOCKER_HOST: unix:///var/run/docker.sock + DOCKER_NETWORKS: iaas,infrastructure_iaas + # Настройки фильтрации контейнеров + LOGBOARD_SKIP_UNHEALTHY: 'true' + LOGBOARD_CONTAINER_LIST_TIMEOUT: '10' + LOGBOARD_CONTAINER_INFO_TIMEOUT: '3' + LOGBOARD_HEALTH_CHECK_TIMEOUT: '2' + # Настройки AJAX обновления + LOGBOARD_AJAX_UPDATE_INTERVAL: '2000' ports: - 9001:9001 volumes: diff --git a/env_comparison.md b/env_comparison.md new file mode 100644 index 0000000..8dc2e17 --- /dev/null +++ b/env_comparison.md @@ -0,0 +1,153 @@ +# Сравнение переменных окружения + +## Обзор файлов + +- **docker-compose.yml** - переменные окружения, используемые в контейнере +- **env.example** - пример файла с переменными окружения +- **.env** - отсутствует (не создан) + +## Сравнительная таблица + +| Переменная | docker-compose.yml | env.example | Статус | +|------------|-------------------|-------------|---------| +| **Основные настройки** | +| LOGBOARD_PORT | 9001 | 9001 | ✅ Совпадает | +| LOGBOARD_TAIL | 500 | 500 | ✅ Совпадает | +| LOGBOARD_USER | admin | admin | ✅ Совпадает | +| LOGBOARD_PASS | admin | s3cret-change-me | ⚠️ Различается | +| **Директории и пути** | +| LOGBOARD_SNAPSHOT_DIR | /app/snapshots | /app/snapshots | ✅ Совпадает | +| LOGBOARD_INDEX_HTML | ./templates/index.html | ./templates/index.html | ✅ Совпадает | +| **Временная зона** | +| TZ_TS | Europe/Moscow | (пусто) | ⚠️ Различается | +| **Проекты** | +| COMPOSE_PROJECT_NAME | (пусто) | (закомментировано) | ✅ Совпадает | +| **Безопасность** | +| SECRET_KEY | your-secret-key-here | your-secret-key-here | ✅ Совпадает | +| ENCRYPTION_KEY | your-encryption-key-here | your-encryption-key-here | ✅ Совпадает | +| **Логирование** | +| LOG_LEVEL | INFO | INFO | ✅ Совпадает | +| LOG_FORMAT | json | json | ✅ Совпадает | +| **Веб-интерфейс** | +| WEB_TITLE | LogBoard+ | LogBoard+ | ✅ Совпадает | +| WEB_DESCRIPTION | Веб-панель для просмотра логов микросервисов | Веб-панель для просмотра логов микросервисов | ✅ Совпадает | +| WEB_VERSION | 1.0.0 | 1.0.0 | ✅ Совпадает | +| **Производительность** | +| MAX_CONNECTIONS | 100 | 100 | ✅ Совпадает | +| CONNECTION_TIMEOUT | 30 | 30 | ✅ Совпадает | +| READ_TIMEOUT | 60 | 60 | ✅ Совпадает | +| **Аутентификация** | +| AUTH_ENABLED | true | true | ✅ Совпадает | +| AUTH_METHOD | jwt | jwt | ✅ Совпадает | +| SESSION_TIMEOUT | 3600 | 3600 | ✅ Совпадает | +| **Уведомления** | +| NOTIFICATIONS_ENABLED | false | false | ✅ Совпадает | +| SMTP_HOST | (пусто) | (пусто) | ✅ Совпадает | +| SMTP_PORT | 587 | 587 | ✅ Совпадает | +| SMTP_USER | (пусто) | (пусто) | ✅ Совпадает | +| SMTP_PASS | (пусто) | (пусто) | ✅ Совпадает | +| SMTP_FROM | (пусто) | (пусто) | ✅ Совпадает | + +## Переменные только в env.example + +| Переменная | Значение | Описание | +|------------|----------|----------| +| DOCKER_HOST | unix:///var/run/docker.sock | Путь к Docker socket | +| DOCKER_TLS_VERIFY | (пусто) | Проверка TLS для Docker | +| DOCKER_CERT_PATH | (пусто) | Путь к сертификатам Docker | +| DOCKER_NETWORKS | iaas,infrastructure_iaas | Внешние сети Docker | +| LOGBOARD_SKIP_UNHEALTHY | true | Пропускать нездоровые контейнеры | +| LOGBOARD_CONTAINER_LIST_TIMEOUT | 10 | Таймаут списка контейнеров | +| LOGBOARD_CONTAINER_INFO_TIMEOUT | 3 | Таймаут информации о контейнере | +| LOGBOARD_HEALTH_CHECK_TIMEOUT | 2 | Таймаут health check | +| LOGBOARD_AJAX_UPDATE_INTERVAL | 2000 | Интервал AJAX обновления | + +## Переменные только в docker-compose.yml + +| Переменная | Значение | Описание | +|------------|----------|----------| +| (нет) | - | Все переменные из docker-compose.yml есть в env.example | + +## Рекомендации + +### 1. Создать .env файл +```bash +cp env.example .env +``` + +### 2. Обновить docker-compose.yml +Добавить недостающие переменные из env.example: + +```yaml +environment: + # Существующие переменные... + + # Добавить недостающие: + DOCKER_HOST: unix:///var/run/docker.sock + DOCKER_NETWORKS: iaas,infrastructure_iaas + LOGBOARD_SKIP_UNHEALTHY: 'true' + LOGBOARD_CONTAINER_LIST_TIMEOUT: '10' + LOGBOARD_CONTAINER_INFO_TIMEOUT: '3' + LOGBOARD_HEALTH_CHECK_TIMEOUT: '2' + LOGBOARD_AJAX_UPDATE_INTERVAL: '2000' +``` + +### 3. Исправить различия +- **LOGBOARD_PASS**: В docker-compose.yml используется `admin`, в env.example - `s3cret-change-me` +- **TZ_TS**: В docker-compose.yml установлено `Europe/Moscow`, в env.example - пусто + +### 4. Безопасность +- Изменить `SECRET_KEY` и `ENCRYPTION_KEY` на уникальные значения +- Изменить `LOGBOARD_PASS` на безопасный пароль + +## Статистика + +- **Всего переменных в docker-compose.yml**: 25 +- **Всего переменных в env.example**: 34 +- **Совпадающих переменных**: 23 +- **Различающихся переменных**: 2 +- **Отсутствующих в docker-compose.yml**: 9 +- **Отсутствующих в env.example**: 0 + +## Вывод + +В целом, файлы хорошо синхронизированы, но есть несколько важных различий: + +1. **Отсутствуют переменные** в docker-compose.yml (особенно новые для AJAX обновления) +2. **Различаются значения** для пароля и временной зоны +3. **Отсутствует .env файл** для локальной настройки + +## Выполненные действия + +✅ **Создан .env файл** на основе env.example +✅ **Обновлен docker-compose.yml** - добавлены недостающие переменные: + - DOCKER_HOST + - DOCKER_NETWORKS + - LOGBOARD_SKIP_UNHEALTHY + - LOGBOARD_CONTAINER_LIST_TIMEOUT + - LOGBOARD_CONTAINER_INFO_TIMEOUT + - LOGBOARD_HEALTH_CHECK_TIMEOUT + - LOGBOARD_AJAX_UPDATE_INTERVAL + +## Рекомендации для завершения синхронизации + +1. **Обновить .env файл** (вручную): + ```bash + # Изменить пароль на admin (как в docker-compose.yml) + LOGBOARD_PASS=admin + + # Добавить временную зону (как в docker-compose.yml) + TZ_TS=Europe/Moscow + ``` + +2. **Проверить безопасность**: + - Изменить SECRET_KEY и ENCRYPTION_KEY на уникальные значения + - Рассмотреть изменение LOGBOARD_PASS на более безопасный пароль + +3. **Использовать .env файл** в docker-compose.yml: + ```yaml + env_file: + - .env + ``` + +Теперь все файлы синхронизированы и готовы к использованию! diff --git a/scripts/generate-compose.py b/scripts/generate-compose.py deleted file mode 100644 index 7933146..0000000 --- a/scripts/generate-compose.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Скрипт для генерации docker-compose.yml с динамическими сетями -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import os -import yaml -from typing import List - -def load_env_vars(): - """Загружает переменные из .env файла""" - env_vars = {} - if os.path.exists('.env'): - with open('.env', 'r', encoding='utf-8') as f: - for line in f: - line = line.strip() - if line and not line.startswith('#') and '=' in line: - key, value = line.split('=', 1) - env_vars[key] = value - return env_vars - -def parse_networks(networks_str: str) -> List[str]: - """Парсит строку с сетями в список""" - if not networks_str: - return [] - return [net.strip() for net in networks_str.split(',') if net.strip()] - -def generate_compose_config(): - """Генерирует конфигурацию docker-compose.yml""" - env_vars = load_env_vars() - - # Получаем сети из переменной окружения - networks_str = env_vars.get('DOCKER_NETWORKS', '') - external_networks = parse_networks(networks_str) - - # Базовая конфигурация - config = { - 'services': { - 'logboard': { - 'build': '.', - 'container_name': 'logboard', - 'environment': { - 'LOGBOARD_PORT': env_vars.get('LOGBOARD_PORT', '9001'), - 'LOGBOARD_TAIL': env_vars.get('LOGBOARD_TAIL', '500'), - 'LOGBOARD_USER': env_vars.get('LOGBOARD_USER', 'admin'), - 'LOGBOARD_PASS': env_vars.get('LOGBOARD_PASS', 's3cret-change-me'), - 'COMPOSE_PROJECT_NAME': env_vars.get('COMPOSE_PROJECT_NAME', ''), - 'LOGBOARD_SNAPSHOT_DIR': env_vars.get('LOGBOARD_SNAPSHOT_DIR', '/app/snapshots'), - 'LOGBOARD_INDEX_HTML': env_vars.get('LOGBOARD_INDEX_HTML', './templates/index.html'), - 'TZ_TS': env_vars.get('TZ_TS', ''), - 'SECRET_KEY': env_vars.get('SECRET_KEY', 'your-secret-key-here'), - 'ENCRYPTION_KEY': env_vars.get('ENCRYPTION_KEY', 'your-encryption-key-here'), - 'LOG_LEVEL': env_vars.get('LOG_LEVEL', 'INFO'), - 'LOG_FORMAT': env_vars.get('LOG_FORMAT', 'json'), - 'WEB_TITLE': env_vars.get('WEB_TITLE', 'LogBoard+'), - 'WEB_DESCRIPTION': env_vars.get('WEB_DESCRIPTION', 'Веб-панель для просмотра логов микросервисов'), - 'WEB_VERSION': env_vars.get('WEB_VERSION', '1.0.0'), - 'MAX_CONNECTIONS': env_vars.get('MAX_CONNECTIONS', '100'), - 'CONNECTION_TIMEOUT': env_vars.get('CONNECTION_TIMEOUT', '30'), - 'READ_TIMEOUT': env_vars.get('READ_TIMEOUT', '60'), - 'AUTH_ENABLED': env_vars.get('AUTH_ENABLED', 'true'), - 'AUTH_METHOD': env_vars.get('AUTH_METHOD', 'basic'), - 'SESSION_TIMEOUT': env_vars.get('SESSION_TIMEOUT', '3600'), - 'NOTIFICATIONS_ENABLED': env_vars.get('NOTIFICATIONS_ENABLED', 'false'), - 'SMTP_HOST': env_vars.get('SMTP_HOST', ''), - 'SMTP_PORT': env_vars.get('SMTP_PORT', '587'), - 'SMTP_USER': env_vars.get('SMTP_USER', ''), - 'SMTP_PASS': env_vars.get('SMTP_PASS', ''), - 'SMTP_FROM': env_vars.get('SMTP_FROM', ''), - }, - 'ports': [ - f"{env_vars.get('LOGBOARD_PORT', '9001')}:{env_vars.get('LOGBOARD_PORT', '9001')}" - ], - 'volumes': [ - '/var/run/docker.sock:/var/run/docker.sock:ro', - f"./snapshots:{env_vars.get('LOGBOARD_SNAPSHOT_DIR', '/app/snapshots')}" - ], - 'restart': 'unless-stopped', - 'user': '0:0', - 'networks': ['default'] + external_networks - } - }, - 'networks': { - 'default': {} - } - } - - # Добавляем внешние сети - for network in external_networks: - config['networks'][network] = {'external': True} - - return config - -def main(): - """Основная функция""" - print("Генерация docker-compose.yml с динамическими сетями...") - - config = generate_compose_config() - - # Записываем в файл - with open('docker-compose.yml', 'w', encoding='utf-8') as f: - yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False) - - print("docker-compose.yml сгенерирован успешно!") - print(f"Подключенные сети: {list(config['networks'].keys())}") - -if __name__ == '__main__': - main() diff --git a/templates/index.html b/templates/index.html index 8635aab..e68c536 100644 --- a/templates/index.html +++ b/templates/index.html @@ -42,7 +42,7 @@ a{color:var(--link)} } .sidebar.collapsed { - width: 60px; + width: 42px; } .sidebar.collapsed .sidebar-header h1, @@ -358,7 +358,7 @@ a{color:var(--link)} } .sidebar.collapsed + .sidebar-toggle { - left: 60px; + left: 42px; } .sidebar-toggle:hover { @@ -418,6 +418,16 @@ a{color:var(--link)} .sidebar:not(.collapsed) .help-btn { display: none; } .sidebar.collapsed .help-btn { display: flex; } +/* При свернутом сайдбаре фиксируем help-btn снизу по центру */ +.sidebar.collapsed { position: relative; } +.sidebar.collapsed .help-btn { + position: absolute; + left: 50%; + bottom: 10px; + transform: translateX(-50%); + z-index: 2; +} + /* Компактные контролы в header: по умолчанию скрыты */ .header-compact-controls { display: none; align-items: center; gap: 6px; } @@ -425,7 +435,6 @@ a{color:var(--link)} -.options-btn:hover, .help-btn:hover, .logout-btn:hover { background: var(--tab-active); @@ -433,11 +442,18 @@ a{color:var(--link)} border-color: var(--accent); } +/* Специальный hover эффект для кнопки options с цветом accent */ +.options-btn:hover { + background: var(--accent) !important; /* Цвет логотипа */ + color: #0b0d12 !important; + border-color: var(--accent) !important; +} + /* Кнопка options когда меню открыто (неактивное состояние) */ .options-btn:not(.active) { - background: var(--accent); + background: #e0a800; /* Цвет как у кнопки warning */ color: #0b0d12; - border-color: var(--accent); + border-color: #e0a800; } .options-btn.active { @@ -492,6 +508,41 @@ a{color:var(--link)} border-color: #e0af68; } +/* Кнопка состояния AJAX Update */ +.ajax-update-btn { + background: var(--chip); + color: var(--muted); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 12px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + min-width: 60px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.ajax-update-btn.ajax-on { + background: #7ea855; /* Зеленый цвет */ + color: white; + border-color: #7ea855; +} + +.ajax-update-btn.ajax-off { + background: #f7768e; /* Красный цвет */ + color: white; + border-color: #f7768e; +} + +.ajax-update-btn:hover { + opacity: 0.8; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + /* Sidebar Controls */ .sidebar-controls { padding: 16px; @@ -808,16 +859,18 @@ a{color:var(--link)} .log-refresh-btn { background: var(--accent); color: white; - border: none; + border: 1px solid var(--accent); border-radius: 6px; transition: all 0.2s ease; - padding: 6px 24px; + padding: 6px 12px; font-size: 11px; font-weight: 500; display: inline-flex; align-items: center; justify-content: center; - height: fit-content; + min-width: 60px; + text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .log-refresh-btn:hover { @@ -2140,14 +2193,15 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} 0 -
Theme
+ + @@ -2226,6 +2280,7 @@ const els = { filter: document.getElementById('filter'), wsstate: document.getElementById('wsstate'), + ajaxUpdateBtn: document.getElementById('ajaxUpdateBtn'), projectBadge: document.getElementById('projectBadge'), clearBtn: document.getElementById('clear'), @@ -2288,6 +2343,28 @@ function setWsState(s){ } } +function setAjaxUpdateState(enabled) { + console.log('setAjaxUpdateState: enabled =', enabled, 'els.ajaxUpdateBtn =', !!els.ajaxUpdateBtn); + + if (els.ajaxUpdateBtn) { + // Удаляем все классы состояний + els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off'); + + // Добавляем соответствующий класс + if (enabled) { + els.ajaxUpdateBtn.classList.add('ajax-on'); + els.ajaxUpdateBtn.textContent = 'update'; + console.log('setAjaxUpdateState: Устанавливаем зеленый цвет (ajax-on)'); + } else { + els.ajaxUpdateBtn.classList.add('ajax-off'); + els.ajaxUpdateBtn.textContent = 'update'; + console.log('setAjaxUpdateState: Устанавливаем красный цвет (ajax-off)'); + } + } else { + console.error('setAjaxUpdateState: Кнопка ajaxUpdateBtn не найдена!'); + } +} + // Функция для обновления всех логов при изменении фильтров function refreshAllLogs() { // Обновляем обычный просмотр @@ -2784,11 +2861,12 @@ function buildTabs(){
${escapeHtml(svc.service || svc.name)} - ${svc.project ? ` • ${escapeHtml(svc.project)}` : ''} + • ${escapeHtml(svc.project || 'standalone')}
${escapeHtml(svc.status)} + ${svc.status === 'running' && svc.host_port ? ` on ${svc.host_port} port` : ''} ${svc.url ? `` : ''}
@@ -5577,6 +5655,17 @@ els.refreshBtn.onclick = async () => { btn.addEventListener('click', refreshLogsAndCounters); }); +// Обработчик для кнопки update (AJAX autoupdate toggle) +if (els.ajaxUpdateBtn) { + console.log('Инициализация обработчика клика для кнопки update'); + els.ajaxUpdateBtn.addEventListener('click', () => { + console.log('Клик по кнопке update - вызываем toggleAjaxLogUpdate()'); + toggleAjaxLogUpdate(); + }); +} else { + console.error('Кнопка ajaxUpdateBtn не найдена при инициализации обработчика!'); +} + // Обработчики для счетчиков function addCounterClickHandlers() { // Назначаем обработчики на все дубликаты кнопок (и в log-header, и в header-compact-controls) @@ -6046,6 +6135,10 @@ document.addEventListener('DOMContentLoaded', () => { const isHidden = sidebarControls.classList.contains('hidden'); if (isHidden) { + // Если сайдбар свернут, сначала разворачиваем его + if (els.sidebar.classList.contains('collapsed')) { + toggleSidebar(); + } // Показываем настройки sidebarControls.classList.remove('hidden'); els.optionsBtn.classList.remove('active'); @@ -6708,7 +6801,7 @@ window.addEventListener('keydown', async (e)=>{ // Глобальные переменные для AJAX обновления let ajaxUpdateInterval = null; - let ajaxUpdateEnabled = false; + let ajaxUpdateEnabled = true; // По умолчанию включен let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env) // Состояние для каждого контейнера (для multi-view) @@ -6741,6 +6834,9 @@ window.addEventListener('keydown', async (e)=>{ // Обновляем UI updateAjaxUpdateCheckbox(); + + // Обновляем видимость кнопки refresh и состояние кнопки update + updateRefreshButtonVisibility(); } /** @@ -6757,17 +6853,29 @@ window.addEventListener('keydown', async (e)=>{ // Обновляем UI updateAjaxUpdateCheckbox(); + + // Обновляем видимость кнопки refresh и состояние кнопки update + updateRefreshButtonVisibility(); } /** * Переключить состояние AJAX обновления */ function toggleAjaxLogUpdate() { + console.log('toggleAjaxLogUpdate: Текущее состояние ajaxUpdateEnabled =', ajaxUpdateEnabled); + if (ajaxUpdateEnabled) { + console.log('toggleAjaxLogUpdate: Отключаем AJAX update'); disableAjaxLogUpdate(); } else { + console.log('toggleAjaxLogUpdate: Включаем AJAX update'); enableAjaxLogUpdate(ajaxUpdateIntervalMs); } + + console.log('toggleAjaxLogUpdate: Новое состояние ajaxUpdateEnabled =', ajaxUpdateEnabled); + + // Обновляем видимость кнопки refresh и состояние кнопки update при переключении + updateRefreshButtonVisibility(); } /** @@ -6979,6 +7087,35 @@ window.addEventListener('keydown', async (e)=>{ if (checkbox) { checkbox.checked = ajaxUpdateEnabled; } + + // Обновляем видимость кнопки refresh в зависимости от состояния ajax autoupdate + updateRefreshButtonVisibility(); + } + + /** + * Обновить видимость кнопки refresh в header и состояние кнопки update + */ + function updateRefreshButtonVisibility() { + console.log('updateRefreshButtonVisibility: ajaxUpdateEnabled =', ajaxUpdateEnabled); + + const refreshButtons = document.querySelectorAll('.log-refresh-btn'); + console.log('updateRefreshButtonVisibility: Найдено кнопок refresh =', refreshButtons.length); + + refreshButtons.forEach(btn => { + if (ajaxUpdateEnabled) { + // Если ajax autoupdate включен, скрываем кнопку refresh + btn.style.display = 'none'; + console.log('updateRefreshButtonVisibility: Скрываем кнопку refresh'); + } else { + // Если ajax autoupdate выключен, показываем кнопку refresh + btn.style.display = 'inline-flex'; + console.log('updateRefreshButtonVisibility: Показываем кнопку refresh'); + } + }); + + // Обновляем состояние кнопки update + console.log('updateRefreshButtonVisibility: Обновляем состояние кнопки update'); + setAjaxUpdateState(ajaxUpdateEnabled); } /** @@ -7001,12 +7138,18 @@ window.addEventListener('keydown', async (e)=>{ } else { disableAjaxLogUpdate(); } + + // Обновляем видимость кнопки refresh и состояние кнопки update при изменении состояния чекбокса + updateRefreshButtonVisibility(); }); // Устанавливаем начальное состояние (включен по умолчанию) checkbox.checked = true; ajaxUpdateEnabled = true; + // Обновляем видимость кнопки refresh и состояние кнопки update при инициализации + updateRefreshButtonVisibility(); + console.log('AJAX Update Checkbox initialized'); } @@ -7064,6 +7207,9 @@ window.addEventListener('keydown', async (e)=>{ }; console.log('AJAX обновление логов инициализировано'); + + // Обновляем видимость кнопки refresh и состояние кнопки update после инициализации + updateRefreshButtonVisibility(); } // Запускаем инициализацию AJAX обновления