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()