feat: запоминание состояния свернутых секций и фильтрация остановленных удаленных контейнеров

- Добавлено запоминание состояния свернутых секций в localStorage
- Функции loadCollapsedSections(), saveCollapsedSections(), updateCollapsedSection()
- Применение сохраненного состояния при загрузке интерфейса
- Фильтрация остановленных удаленных контейнеров (неактивные более 5 минут)
- Обновлена функция get_remote_containers() для проверки активности
- Исправлен запуск контейнера (убрана зависимость от start.sh)
- Добавлена команда uvicorn в docker-compose.yml

Новые возможности:
 Состояние свернутых секций сохраняется между сессиями
 Остановленные удаленные контейнеры автоматически скрываются
 Контейнеры считаются неактивными после 5 минут без обновления логов
 Интерфейс стал более стабильным и удобным

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов 2025-08-20 20:43:53 +03:00
parent a1529f4c4e
commit 769d33777d
3 changed files with 130 additions and 25 deletions

View File

@ -289,21 +289,29 @@ def get_remote_containers(hostname: str) -> List[Dict]:
file_path = os.path.join(remote_logs_dir, filename) file_path = os.path.join(remote_logs_dir, filename)
stat = os.stat(file_path) stat = os.stat(file_path)
containers.append({ # Проверяем, активен ли контейнер (логи обновлялись в последние 5 минут)
"id": f"remote-{hostname}-{container_name}", import time
"name": container_name, current_time = time.time()
"status": "running", # Предполагаем, что удаленные контейнеры работают last_modified = stat.st_mtime
"image": "remote", is_active = (current_time - last_modified) < 300 # 5 минут = 300 секунд
"service": container_name,
"project": "remote", # Добавляем контейнер только если он активен или если include_stopped=True
"health": "healthy", if is_active or include_stopped:
"ports": [], containers.append({
"url": None, "id": f"remote-{hostname}-{container_name}",
"hostname": hostname, "name": container_name,
"is_remote": True, "status": "running" if is_active else "stopped",
"last_modified": stat.st_mtime, "image": "remote",
"size": stat.st_size "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: except Exception as e:
docker_logger.error(f"Ошибка получения контейнеров для хоста {hostname}: {e}") docker_logger.error(f"Ошибка получения контейнеров для хоста {hostname}: {e}")

View File

@ -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 соединения в интерфейсе * Устанавливает состояние WebSocket соединения в интерфейсе
* @param {string} s - Состояние: 'on', 'off', 'err', 'available' * @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`); const hostContent = document.getElementById(`host-${hostname}-content`);
if (hostContent) { 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'; hostContent.style.display = 'block';
} }
}); }
});
// Устанавливаем отображение секции удаленных контейнеров // Применяем сохраненное состояние для секции удаленных контейнеров
const remoteContent = document.getElementById('remote-content'); const remoteContent = document.getElementById('remote-content');
if (remoteContent) { if (remoteContent) {
const collapsedState = loadCollapsedSections();
const isRemoteCollapsed = collapsedState.remote || false;
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'; remoteContent.style.display = 'block';
} }
}
// Устанавливаем отображение секции локальных контейнеров // Применяем сохраненное состояние для секции локальных контейнеров
const localContent = document.getElementById('local-content'); const localContent = document.getElementById('local-content');
if (localContent) { if (localContent) {
const collapsedState = loadCollapsedSections();
const isLocalCollapsed = collapsedState.local || false;
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'; localContent.style.display = 'block';
} }
} }
}
// Создаем миникарточки для всех контейнеров // Создаем миникарточки для всех контейнеров
if (miniContainerList) { if (miniContainerList) {
@ -6098,11 +6189,13 @@ function reinitializeElements() {
content.style.display = 'block'; content.style.display = 'block';
icon.className = 'fas fa-chevron-down'; icon.className = 'fas fa-chevron-down';
button.title = 'Свернуть секцию'; button.title = 'Свернуть секцию';
updateCollapsedSection(target, false);
} else { } else {
// Сворачиваем секцию // Сворачиваем секцию
content.style.display = 'none'; content.style.display = 'none';
icon.className = 'fas fa-chevron-right'; icon.className = 'fas fa-chevron-right';
button.title = 'Развернуть секцию'; button.title = 'Развернуть секцию';
updateCollapsedSection(target, true);
} }
} }
} }
@ -6132,11 +6225,13 @@ function reinitializeElements() {
content.style.display = 'block'; content.style.display = 'block';
icon.className = 'fas fa-chevron-down'; icon.className = 'fas fa-chevron-down';
button.title = 'Свернуть секцию'; button.title = 'Свернуть секцию';
updateCollapsedSection(target, false);
} else { } else {
// Сворачиваем секцию // Сворачиваем секцию
content.style.display = 'none'; content.style.display = 'none';
icon.className = 'fas fa-chevron-right'; icon.className = 'fas fa-chevron-right';
button.title = 'Развернуть секцию'; button.title = 'Развернуть секцию';
updateCollapsedSection(target, true);
} }
} }
} }

View File

@ -10,11 +10,13 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots - ./snapshots:/app/snapshots
- ./logs:/app/logs - ./logs:/app/logs
- ./app:/app
restart: unless-stopped restart: unless-stopped
user: 0:0 user: 0:0
networks: networks:
- iaas - iaas
- infrastructure_iaas - infrastructure_iaas
command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "${LOGBOARD_PORT}"]
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${LOGBOARD_PORT}/healthz"] test: ["CMD", "curl", "-f", "http://localhost:${LOGBOARD_PORT}/healthz"]
interval: 30s interval: 30s