feat: Добавлено AJAX обновление логов и улучшения интерфейса

Основные изменения:
- Добавлено AJAX обновление логов с чекбоксом 'Auto-update logs'
- Добавлена опция 'All logs' в выпадающий список tail lines
- Исправлено отображение длинных названий контейнеров в multi-view режиме
- Восстановлена загрузка истории логов при включенном AJAX обновлении

Новые функции:
- Чекбокс 'Auto-update logs' в секции Options (включен по умолчанию)
- Настройка интервала обновления через LOGBOARD_AJAX_UPDATE_INTERVAL
- API эндпоинт /api/settings для получения настроек приложения
- Поддержка параметра tail=all для загрузки всех логов
- Автоматический запуск AJAX обновления при включении чекбокса

Исправления UI:
- Кнопки LogLevels не уезжают вправо при длинных названиях контейнеров
- Добавлено обрезание длинных названий с многоточием
- Фиксированная высота заголовков в multi-view режиме
- Защита от сжатия кнопок LogLevels

Тестирование:
- Добавлены тесты для AJAX обновления (test_ajax_update.py)
- Тест multi-view AJAX обновления (test_multi_view_ajax.py)
- Тест опции 'all logs' (test_all_logs.py)
- Тест отображения длинных названий (test_multi_view_layout.py)
- Команды make test-ajax, make test-multi-view-ajax, make test-all-logs, make test-multi-view-layout

Документация:
- Создана подробная документация AJAX обновления (app/docs/ajax-update.md)
- Обновлен CHANGELOG.md с версиями 1.3.0, 1.5.0, 1.6.0
- Обновлен README.md с описанием новых функций

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-08-18 19:35:47 +03:00
parent 2d565d52a6
commit 6e51f00791
14 changed files with 2066 additions and 7 deletions

View File

@@ -0,0 +1,163 @@
#!/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)