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:
parent
a1529f4c4e
commit
769d33777d
@ -289,19 +289,27 @@ def get_remote_containers(hostname: str) -> List[Dict]:
|
||||
file_path = os.path.join(remote_logs_dir, filename)
|
||||
stat = os.stat(file_path)
|
||||
|
||||
# Проверяем, активен ли контейнер (логи обновлялись в последние 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", # Предполагаем, что удаленные контейнеры работают
|
||||
"status": "running" if is_active else "stopped",
|
||||
"image": "remote",
|
||||
"service": container_name,
|
||||
"project": "remote",
|
||||
"health": "healthy",
|
||||
"health": "healthy" if is_active else "unhealthy",
|
||||
"ports": [],
|
||||
"url": None,
|
||||
"hostname": hostname,
|
||||
"is_remote": True,
|
||||
"last_modified": stat.st_mtime,
|
||||
"last_modified": last_modified,
|
||||
"size": stat.st_size
|
||||
})
|
||||
except Exception as e:
|
||||
|
@ -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 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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user