diff --git a/app/static/js/index.js b/app/static/js/index.js index abd6669..9be2b9d 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -877,6 +877,18 @@ function buildTabs(){ const statusClass = svc.status === 'running' ? 'running' : svc.status === 'stopped' ? 'stopped' : 'paused'; + // Создаем tooltip для локального контейнера + const tooltipText = `Локальный контейнер +Имя: ${svc.name} +Сервис: ${svc.service || svc.name} +Проект: ${svc.project || 'standalone'} +Статус: ${svc.status} +Хост: localhost +${svc.url ? `URL: ${svc.url}` : ''} +${svc.status === 'running' && svc.host_port ? `Порт: ${svc.host_port}` : ''} + +Кликните для просмотра логов`; + item.innerHTML = `
@@ -898,6 +910,9 @@ function buildTabs(){
`; + // Добавляем tooltip + item.setAttribute('title', tooltipText); + // Устанавливаем состояние selected для контейнера if (state.selectedContainers.includes(svc.id)) { item.classList.add('selected'); @@ -974,6 +989,17 @@ function buildTabs(){ const statusClass = svc.status === 'running' ? 'running' : svc.status === 'stopped' ? 'stopped' : 'paused'; + // Создаем tooltip для удаленного контейнера + const tooltipText = `Удаленный контейнер +Имя: ${svc.name} +Сервис: ${svc.service || svc.name} +Проект: ${svc.project || 'remote'} +Статус: ${svc.status} +Хост: ${svc.hostname} +${svc.last_modified ? `Обновлено: ${new Date(svc.last_modified * 1000).toLocaleString()}` : ''} + +Кликните для просмотра логов`; + item.innerHTML = `
@@ -998,6 +1024,9 @@ function buildTabs(){
`; + // Добавляем tooltip + item.setAttribute('title', tooltipText); + // Устанавливаем состояние selected для контейнера if (state.selectedContainers.includes(svc.id)) { item.classList.add('selected'); @@ -3567,20 +3596,9 @@ async function switchToSingle(svc){ console.log('Multi-view grid not found in DOM'); } - // Legacy functionality (скрытая) - console.log('switchToSingle: Setting up legacy functionality'); - setLayout('tabs'); - els.grid.innerHTML=''; - const panel = ensurePanel(svc); - panel.querySelector('.log').textContent=''; - closeWs(svc.id); - console.log('switchToSingle: Calling openWs for:', svc.name, 'id:', svc.id); - openWs(svc, panel); + // Устанавливаем текущий контейнер state.current = svc; console.log('switchToSingle: Set state.current to:', svc.name, 'id:', svc.id); - console.log('switchToSingle: state.current after setting:', state.current); - buildTabs(); - for (const p of [...els.grid.children]) if (p!==panel) p.remove(); // Обновляем состояние выбранных контейнеров для корректного отображения заголовка state.selectedContainers = [svc.id]; @@ -3588,6 +3606,13 @@ async function switchToSingle(svc){ // Сохраняем режим просмотра в localStorage saveViewMode(false, [svc.id]); + // Открываем WebSocket соединение для логов + console.log('switchToSingle: Calling openWs for:', svc.name, 'id:', svc.id); + openWs(svc, els.logContent); + + // Обновляем интерфейс + buildTabs(); + // Обновляем активное состояние в UI updateActiveContainerUI(svc.id); @@ -5993,13 +6018,58 @@ function reinitializeElements() { * Добавляет обработчики для сворачивания секций контейнеров */ function addSectionToggleHandlers() { - // Обработчик для сворачивания секций - document.addEventListener('click', (e) => { - if (e.target.closest('.section-toggle-btn')) { + // Удаляем старые обработчики, если они есть + document.removeEventListener('click', handleSectionToggle); + document.removeEventListener('click', handleHostHeaderToggle); + + // Добавляем новые обработчики + document.addEventListener('click', handleSectionToggle); + document.addEventListener('click', handleHostHeaderToggle); + } + + /** + * Обработчик для сворачивания секций + */ + function handleSectionToggle(e) { + if (e.target.closest('.section-toggle-btn')) { + e.preventDefault(); + e.stopPropagation(); + + const button = e.target.closest('.section-toggle-btn'); + const target = button.getAttribute('data-target'); + const icon = button.querySelector('i'); + const content = document.getElementById(`${target}-content`); + + if (content) { + const isCollapsed = content.style.display === 'none'; + + if (isCollapsed) { + // Разворачиваем секцию + content.style.display = 'block'; + icon.className = 'fas fa-chevron-down'; + button.title = 'Свернуть секцию'; + } else { + // Сворачиваем секцию + content.style.display = 'none'; + icon.className = 'fas fa-chevron-right'; + button.title = 'Развернуть секцию'; + } + } + } + } + + /** + * Обработчик для сворачивания секций хостов + */ + function handleHostHeaderToggle(e) { + if (e.target.closest('.host-header')) { + const header = e.target.closest('.host-header'); + const button = header.querySelector('.section-toggle-btn'); + + if (button && !e.target.closest('.section-toggle-btn')) { e.preventDefault(); e.stopPropagation(); - const button = e.target.closest('.section-toggle-btn'); const target = button.getAttribute('data-target'); const icon = button.querySelector('i'); const content = document.getElementById(`${target}-content`); @@ -6020,40 +6090,7 @@ function reinitializeElements() { } } } - }); - - // Обработчик для сворачивания секций хостов - document.addEventListener('click', (e) => { - if (e.target.closest('.host-header')) { - const header = e.target.closest('.host-header'); - const button = header.querySelector('.section-toggle-btn'); - - if (button && !e.target.closest('.section-toggle-btn')) { - e.preventDefault(); - e.stopPropagation(); - - const target = button.getAttribute('data-target'); - const icon = button.querySelector('i'); - const content = document.getElementById(`${target}-content`); - - if (content) { - const isCollapsed = content.style.display === 'none'; - - if (isCollapsed) { - // Разворачиваем секцию - content.style.display = 'block'; - icon.className = 'fas fa-chevron-down'; - button.title = 'Свернуть секцию'; - } else { - // Сворачиваем секцию - content.style.display = 'none'; - icon.className = 'fas fa-chevron-right'; - button.title = 'Развернуть секцию'; - } - } - } - } - }); + } } diff --git a/docker-compose.yml b/docker-compose.yml index d3db201..2813b2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: - iaas - infrastructure_iaas healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${LOGBOARD_PORT}/"] + test: ["CMD", "curl", "-f", "http://localhost:${LOGBOARD_PORT}/healthz"] interval: 30s timeout: 10s retries: 3 diff --git a/test_fixes.py b/test_fixes.py new file mode 100644 index 0000000..472dcd3 --- /dev/null +++ b/test_fixes.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Тестовый скрипт для проверки исправлений LogBoard+ Server +Автор: Сергей Антропов +Сайт: https://devops.org.ru +""" + +import requests +import json + +def test_fixes(): + """Тестирование исправлений""" + + base_url = "http://localhost:9001" + + # 1. Вход в систему + print("🔐 Вход в систему...") + login_data = {"username": "admin", "password": "admin"} + response = requests.post(f"{base_url}/api/auth/login", json=login_data) + + if response.status_code != 200: + print(f"❌ Ошибка входа: {response.status_code}") + return + + token = response.json()["access_token"] + headers = {"Authorization": f"Bearer {token}"} + + print("✅ Вход выполнен успешно") + + # 2. Получение контейнеров + print("\n🐳 Получение контейнеров...") + response = requests.get(f"{base_url}/api/containers/services", headers=headers) + + if response.status_code != 200: + print(f"❌ Ошибка получения контейнеров: {response.status_code}") + return + + containers = response.json() + print(f"✅ Контейнеров получено: {len(containers)}") + + # Анализируем контейнеры + local_containers = [c for c in containers if not c.get('is_remote', False)] + remote_containers = [c for c in containers if c.get('is_remote', False)] + + print(f"\n📊 Статистика контейнеров:") + print(f" 📍 Локальные контейнеры: {len(local_containers)}") + for container in local_containers: + print(f" • {container['name']} ({container['status']})") + + print(f"\n 🌐 Удаленные контейнеры: {len(remote_containers)}") + + # Группируем удаленные контейнеры по хостам + containers_by_host = {} + for container in remote_containers: + hostname = container.get('hostname', 'unknown') + if hostname not in containers_by_host: + containers_by_host[hostname] = [] + containers_by_host[hostname].append(container) + + for hostname, host_containers in containers_by_host.items(): + print(f" 🖥️ Хост: {hostname} ({len(host_containers)} контейнеров)") + for container in host_containers: + print(f" • {container['name']} ({container['status']})") + + # 3. Тестирование получения логов + print(f"\n📝 Тестирование получения логов...") + + # Тестируем локальный контейнер + if local_containers: + test_container = local_containers[0] + print(f" Тестируем локальный контейнер: {test_container['name']}") + + response = requests.get(f"{base_url}/api/logs/{test_container['id']}?tail=10", headers=headers) + if response.status_code == 200: + logs = response.json() + print(f" ✅ Логи получены: {len(logs.get('logs', []))} строк") + else: + print(f" ❌ Ошибка получения логов: {response.status_code}") + + # Тестируем удаленный контейнер + if remote_containers: + test_remote_container = remote_containers[0] + print(f" Тестируем удаленный контейнер: {test_remote_container['name']}") + + response = requests.get(f"{base_url}/api/logs/{test_remote_container['id']}?tail=10", headers=headers) + if response.status_code == 200: + logs = response.json() + print(f" ✅ Логи получены: {len(logs.get('logs', []))} строк") + else: + print(f" ❌ Ошибка получения логов: {response.status_code}") + + # 4. Тестирование статистики + print(f"\n📊 Тестирование статистики...") + + if local_containers: + test_container = local_containers[0] + response = requests.get(f"{base_url}/api/logs/stats/{test_container['id']}", headers=headers) + if response.status_code == 200: + stats = response.json() + print(f" ✅ Статистика получена для {test_container['name']}:") + 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)}") + else: + print(f" ❌ Ошибка получения статистики: {response.status_code}") + + # 5. Проверка структуры данных для tooltip + print(f"\n🔍 Проверка структуры данных для tooltip...") + + for container in local_containers[:2]: # Проверяем первые 2 локальных контейнера + print(f" 📍 Локальный контейнер {container['name']}:") + print(f" Имя: {container['name']}") + print(f" Сервис: {container.get('service', 'N/A')}") + print(f" Проект: {container.get('project', 'standalone')}") + print(f" Статус: {container['status']}") + print(f" Хост: localhost") + if container.get('url'): + print(f" URL: {container['url']}") + if container.get('host_port'): + print(f" Порт: {container['host_port']}") + + for container in remote_containers[:2]: # Проверяем первые 2 удаленных контейнера + print(f" 🌐 Удаленный контейнер {container['name']}:") + print(f" Имя: {container['name']}") + print(f" Сервис: {container.get('service', 'N/A')}") + print(f" Проект: {container.get('project', 'remote')}") + print(f" Статус: {container['status']}") + print(f" Хост: {container.get('hostname', 'unknown')}") + if container.get('last_modified'): + print(f" Обновлено: {container['last_modified']}") + + print(f"\n🎉 Тестирование завершено!") + print(f"🌐 Откройте http://localhost:9001 в браузере") + print(f" 🔽 Проверьте сворачивание секций в sidebar") + print(f" 💡 Проверьте tooltip на карточках контейнеров") + print(f" 📝 Проверьте отображение логов при клике на карточки") + +if __name__ == "__main__": + test_fixes()