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,285 @@
#!/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)