From 769d33777d14cae615be8904b85ca1a22eddfe22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=90=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=BE=D0=B2?= Date: Wed, 20 Aug 2025 20:43:53 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B2=D0=B5=D1=80=D0=BD=D1=83?= =?UTF-8?q?=D1=82=D1=8B=D1=85=20=D1=81=D0=B5=D0=BA=D1=86=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BE=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлено запоминание состояния свернутых секций в localStorage - Функции loadCollapsedSections(), saveCollapsedSections(), updateCollapsedSection() - Применение сохраненного состояния при загрузке интерфейса - Фильтрация остановленных удаленных контейнеров (неактивные более 5 минут) - Обновлена функция get_remote_containers() для проверки активности - Исправлен запуск контейнера (убрана зависимость от start.sh) - Добавлена команда uvicorn в docker-compose.yml Новые возможности: ✅ Состояние свернутых секций сохраняется между сессиями ✅ Остановленные удаленные контейнеры автоматически скрываются ✅ Контейнеры считаются неактивными после 5 минут без обновления логов ✅ Интерфейс стал более стабильным и удобным Автор: Сергей Антропов Сайт: https://devops.org.ru --- app/core/docker.py | 38 ++++++++------ app/static/js/index.js | 115 +++++++++++++++++++++++++++++++++++++---- docker-compose.yml | 2 + 3 files changed, 130 insertions(+), 25 deletions(-) diff --git a/app/core/docker.py b/app/core/docker.py index e2fa2a7..f6f33c6 100644 --- a/app/core/docker.py +++ b/app/core/docker.py @@ -289,21 +289,29 @@ def get_remote_containers(hostname: str) -> List[Dict]: file_path = os.path.join(remote_logs_dir, filename) stat = os.stat(file_path) - containers.append({ - "id": f"remote-{hostname}-{container_name}", - "name": container_name, - "status": "running", # Предполагаем, что удаленные контейнеры работают - "image": "remote", - "service": container_name, - "project": "remote", - "health": "healthy", - "ports": [], - "url": None, - "hostname": hostname, - "is_remote": True, - "last_modified": stat.st_mtime, - "size": stat.st_size - }) + # Проверяем, активен ли контейнер (логи обновлялись в последние 5 минут) + import time + current_time = time.time() + last_modified = stat.st_mtime + is_active = (current_time - last_modified) < 300 # 5 минут = 300 секунд + + # Добавляем контейнер только если он активен или если include_stopped=True + if is_active or include_stopped: + containers.append({ + "id": f"remote-{hostname}-{container_name}", + "name": container_name, + "status": "running" if is_active else "stopped", + "image": "remote", + "service": container_name, + "project": "remote", + "health": "healthy" if is_active else "unhealthy", + "ports": [], + "url": None, + "hostname": hostname, + "is_remote": True, + "last_modified": last_modified, + "size": stat.st_size + }) except Exception as e: docker_logger.error(f"Ошибка получения контейнеров для хоста {hostname}: {e}") diff --git a/app/static/js/index.js b/app/static/js/index.js index 7031359..36e2500 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -98,6 +98,58 @@ function filterStoppedContainers(containers) { }); } +/** + * Загружает состояние свернутых секций из localStorage + * @returns {Object} Объект с состоянием секций + */ +function loadCollapsedSections() { + try { + const saved = localStorage.getItem('lb_collapsed_sections'); + return saved ? JSON.parse(saved) : { + local: false, + remote: false, + hosts: {} + }; + } catch (e) { + console.warn('Ошибка загрузки состояния секций:', e); + return { + local: false, + remote: false, + hosts: {} + }; + } +} + +/** + * Сохраняет состояние свернутых секций в localStorage + * @param {Object} collapsedState - Объект с состоянием секций + */ +function saveCollapsedSections(collapsedState) { + try { + localStorage.setItem('lb_collapsed_sections', JSON.stringify(collapsedState)); + } catch (e) { + console.warn('Ошибка сохранения состояния секций:', e); + } +} + +/** + * Обновляет состояние свернутой секции + * @param {string} sectionType - Тип секции ('local', 'remote', 'host-{hostname}') + * @param {boolean} isCollapsed - Свернута ли секция + */ +function updateCollapsedSection(sectionType, isCollapsed) { + const collapsedState = loadCollapsedSections(); + + if (sectionType.startsWith('host-')) { + const hostname = sectionType.replace('host-', ''); + collapsedState.hosts[hostname] = isCollapsed; + } else { + collapsedState[sectionType] = isCollapsed; + } + + saveCollapsedSections(collapsedState); +} + /** * Устанавливает состояние WebSocket соединения в интерфейсе * @param {string} s - Состояние: 'on', 'off', 'err', 'available' @@ -1064,25 +1116,64 @@ ${svc.last_modified ? `Обновлено: ${new Date(svc.last_modified * 1000). } }); - // Устанавливаем отображение секции хоста - const hostContent = document.getElementById(`host-${hostname}-content`); - if (hostContent) { + // Применяем сохраненное состояние для секции хоста + const hostContent = document.getElementById(`host-${hostname}-content`); + if (hostContent) { + const collapsedState = loadCollapsedSections(); + const isHostCollapsed = collapsedState.hosts[hostname] || false; + + if (isHostCollapsed) { + hostContent.style.display = 'none'; + const hostButton = document.querySelector(`[data-target="host-${hostname}"] .section-toggle-btn`); + if (hostButton) { + const icon = hostButton.querySelector('i'); + icon.className = 'fas fa-chevron-right'; + hostButton.title = 'Развернуть секцию'; + } + } else { hostContent.style.display = 'block'; } - }); + } + }); + + // Применяем сохраненное состояние для секции удаленных контейнеров + const remoteContent = document.getElementById('remote-content'); + if (remoteContent) { + const collapsedState = loadCollapsedSections(); + const isRemoteCollapsed = collapsedState.remote || false; - // Устанавливаем отображение секции удаленных контейнеров - const remoteContent = document.getElementById('remote-content'); - if (remoteContent) { + if (isRemoteCollapsed) { + remoteContent.style.display = 'none'; + const remoteButton = document.querySelector('[data-target="remote"] .section-toggle-btn'); + if (remoteButton) { + const icon = remoteButton.querySelector('i'); + icon.className = 'fas fa-chevron-right'; + remoteButton.title = 'Развернуть секцию'; + } + } else { remoteContent.style.display = 'block'; } + } + + // Применяем сохраненное состояние для секции локальных контейнеров + const localContent = document.getElementById('local-content'); + if (localContent) { + const collapsedState = loadCollapsedSections(); + const isLocalCollapsed = collapsedState.local || false; - // Устанавливаем отображение секции локальных контейнеров - const localContent = document.getElementById('local-content'); - if (localContent) { + if (isLocalCollapsed) { + localContent.style.display = 'none'; + const localButton = document.querySelector('[data-target="local"] .section-toggle-btn'); + if (localButton) { + const icon = localButton.querySelector('i'); + icon.className = 'fas fa-chevron-right'; + localButton.title = 'Развернуть секцию'; + } + } else { localContent.style.display = 'block'; } } + } // Создаем миникарточки для всех контейнеров if (miniContainerList) { @@ -6098,11 +6189,13 @@ function reinitializeElements() { content.style.display = 'block'; icon.className = 'fas fa-chevron-down'; button.title = 'Свернуть секцию'; + updateCollapsedSection(target, false); } else { // Сворачиваем секцию content.style.display = 'none'; icon.className = 'fas fa-chevron-right'; button.title = 'Развернуть секцию'; + updateCollapsedSection(target, true); } } } @@ -6132,11 +6225,13 @@ function reinitializeElements() { content.style.display = 'block'; icon.className = 'fas fa-chevron-down'; button.title = 'Свернуть секцию'; + updateCollapsedSection(target, false); } else { // Сворачиваем секцию content.style.display = 'none'; icon.className = 'fas fa-chevron-right'; button.title = 'Развернуть секцию'; + updateCollapsedSection(target, true); } } } diff --git a/docker-compose.yml b/docker-compose.yml index 2813b2f..211c4eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,13 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro - ./snapshots:/app/snapshots - ./logs:/app/logs + - ./app:/app restart: unless-stopped user: 0:0 networks: - iaas - infrastructure_iaas + command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "${LOGBOARD_PORT}"] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${LOGBOARD_PORT}/healthz"] interval: 30s