feat: улучшения UI/UX LogBoard+
- Добавлена кнопка Update для управления AJAX auto-update - AJAX auto-update включен по умолчанию - Улучшено управление видимостью кнопки Refresh - Переупорядочены кнопки в header (Update, Refresh) - Унифицированы стили кнопок (высота, шрифт, границы) - Добавлен hover эффект для кнопки options с цветом warning - Позиционирование help-btn в свернутом sidebar - Уменьшена ширина свернутого sidebar на 30% - Добавлена логика разворачивания sidebar при клике на options - Отображение внешнего порта в статусе контейнера - Показ 'standalone' для контейнеров без проекта - Обновлена документация Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user