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
+
+```
+
+## Визуальное представление
+
+### 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 обновления