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)
|
file_path = os.path.join(remote_logs_dir, filename)
|
||||||
stat = os.stat(file_path)
|
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({
|
containers.append({
|
||||||
"id": f"remote-{hostname}-{container_name}",
|
"id": f"remote-{hostname}-{container_name}",
|
||||||
"name": container_name,
|
"name": container_name,
|
||||||
"status": "running", # Предполагаем, что удаленные контейнеры работают
|
"status": "running" if is_active else "stopped",
|
||||||
"image": "remote",
|
"image": "remote",
|
||||||
"service": container_name,
|
"service": container_name,
|
||||||
"project": "remote",
|
"project": "remote",
|
||||||
"health": "healthy",
|
"health": "healthy" if is_active else "unhealthy",
|
||||||
"ports": [],
|
"ports": [],
|
||||||
"url": None,
|
"url": None,
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"is_remote": True,
|
"is_remote": True,
|
||||||
"last_modified": stat.st_mtime,
|
"last_modified": last_modified,
|
||||||
"size": stat.st_size
|
"size": stat.st_size
|
||||||
})
|
})
|
||||||
except Exception as e:
|
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 соединения в интерфейсе
|
* Устанавливает состояние 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user