Исправление ошибки 500 при сохранении исключенных контейнеров

- Исправлен путь к файлу excluded_containers.json в функциях load_excluded_containers и save_excluded_containers
- Добавлено создание директории при необходимости
- Улучшено логирование операций с файлом
- Протестирована функциональность API endpoints
- Обновлен список исключенных контейнеров

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-09-01 18:18:09 +03:00
parent 4077142f79
commit d570807c02
4 changed files with 319 additions and 42 deletions

View File

@@ -15,6 +15,7 @@ const state = {
services: [], // Список всех доступных сервисов
current: null, // Текущий выбранный контейнер для single view
open: {}, // Открытые WebSocket соединения: id -> {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName}
wsConnections: {}, // WebSocket соединения для multiview: id -> WebSocket
layout: 'tabs', // Режим отображения: 'tabs' | 'grid2' | 'grid3' | 'grid4'
filter: null, // Текущий фильтр для логов
levels: {debug:true, info:true, warn:true, err:true, other:true}, // Уровни логирования для отображения
@@ -33,6 +34,7 @@ const els = {
tail: document.getElementById('tail'), // Поле ввода количества строк логов
autoscroll: document.getElementById('autoscroll'), // Чекбокс автопрокрутки
wrapToggle: document.getElementById('wrap'), // Переключатель переноса строк
autoRefreshOnRestore: document.getElementById('autoRefreshOnRestore'), // Чекбокс автообновления при восстановлении
filter: document.getElementById('filter'), // Поле фильтра логов
wsstate: document.getElementById('wsstate'), // Индикатор состояния WebSocket
@@ -369,6 +371,8 @@ function refreshAllLogs() {
} else {
recalculateCounters();
}
// Прокручиваем к последним логам после обновления
scrollToBottom();
}, 100);
}
/**
@@ -662,20 +666,28 @@ function updateHeaderCounters(containerId, counters) {
// Функция для инициализации состояния кнопок уровней логирования
function initializeLevelButtons() {
console.log('initializeLevelButtons: Starting initialization...');
console.log('initializeLevelButtons: Current multiViewMode:', state.multiViewMode);
console.log('initializeLevelButtons: Current selectedContainers:', state.selectedContainers);
// Восстанавливаем состояние кнопок loglevels из localStorage
const savedLevelsState = getLogLevelsStateFromStorage();
if (savedLevelsState) {
console.log('Restoring log levels state from localStorage');
console.log('initializeLevelButtons: Restoring log levels state from localStorage:', savedLevelsState);
// Восстанавливаем глобальные настройки для single-view
if (savedLevelsState.globalLevels) {
state.levels = { ...state.levels, ...savedLevelsState.globalLevels };
console.log('initializeLevelButtons: Restored global levels:', state.levels);
}
// Восстанавливаем настройки контейнеров для multi-view
if (savedLevelsState.containerLevels) {
state.containerLevels = { ...state.containerLevels, ...savedLevelsState.containerLevels };
console.log('initializeLevelButtons: Restored container levels:', state.containerLevels);
}
} else {
console.log('initializeLevelButtons: No saved levels state found in localStorage');
}
// Инициализируем кнопки для single-view
@@ -689,24 +701,33 @@ function initializeLevelButtons() {
// Инициализируем кнопки для multi-view (если есть)
const multiLevelBtns = document.querySelectorAll('.multi-view-levels .level-btn');
multiLevelBtns.forEach(btn => {
console.log(`initializeLevelButtons: Found ${multiLevelBtns.length} multi-view level buttons`);
multiLevelBtns.forEach((btn, index) => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
console.log(`initializeLevelButtons: Processing button ${index + 1}: level=${level}, containerId=${containerId}`);
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
console.log(`initializeLevelButtons: Initialized container levels for ${containerId}:`, state.containerLevels[containerId]);
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
console.log(`initializeLevelButtons: Setting button state: level=${level}, containerId=${containerId}, isActive=${isActive}`);
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
console.log(`initializeLevelButtons: Button classes after toggle:`, btn.className);
});
// Обновляем стили логов после инициализации кнопок
@@ -714,6 +735,12 @@ function initializeLevelButtons() {
// Применяем настройки wrap text
applyWrapSettings();
// Устанавливаем обработчик событий для кнопок уровней логирования
if (window.levelButtonClickHandler) {
document.addEventListener('click', window.levelButtonClickHandler);
console.log('initializeLevelButtons: Level button click handler installed');
}
}
/**
* Применяет фильтр к строке лога
@@ -1809,10 +1836,13 @@ async function setupMultiView() {
// Создаем панели для каждого выбранного контейнера
console.log(`setupMultiView: Creating panels for ${state.selectedContainers.length} containers:`, state.selectedContainers);
console.log(`setupMultiView: Available services:`, state.services.map(s => ({ id: s.id, name: s.name })));
state.selectedContainers.forEach((containerId, index) => {
const service = state.services.find(s => s.id === containerId);
if (!service) {
console.error(`setupMultiView: Service not found for container ID: ${containerId}`);
console.error(`setupMultiView: Available service IDs:`, state.services.map(s => s.id));
return;
}
@@ -1869,10 +1899,16 @@ async function setupMultiView() {
// Применяем сохраненный порядок панелей
setTimeout(() => {
console.log('setupMultiView: Starting panel order restoration...');
console.log('setupMultiView: Current selectedContainers:', state.selectedContainers);
console.log('setupMultiView: Current multiViewMode:', state.multiViewMode);
// Сначала очищаем дубликаты, если они есть
cleanupDuplicatePanels();
// Затем применяем порядок
applyPanelOrder();
console.log('setupMultiView: Panel order restoration completed');
}, 100); // Небольшая задержка для завершения создания панелей
// Обновляем счетчики для multi view
@@ -1946,30 +1982,22 @@ function createMultiViewPanel(service) {
}
// Добавляем drag & drop функциональность
setupDragAndDrop(panel);
if (window.setupDragAndDrop) {
window.setupDragAndDrop(panel);
} else {
console.warn('setupDragAndDrop not available yet, will be set up later');
// Устанавливаем drag & drop позже, когда функция будет доступна
setTimeout(() => {
if (window.setupDragAndDrop) {
window.setupDragAndDrop(panel);
}
}, 100);
}
// Инициализируем состояние кнопок уровней логирования для этого контейнера
setTimeout(() => {
const levelBtns = panel.querySelectorAll('.level-btn');
levelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
console.log(`createMultiViewPanel: Initializing level buttons for ${service.name} (${service.id})`);
initializeLevelButtons();
}, 100);
console.log(`Multi-view panel created for ${service.name}`);
@@ -1993,10 +2021,24 @@ function openMultiViewWs(service) {
console.log(`openMultiViewWs: Current multiViewMode: ${state.multiViewMode}`);
console.log(`openMultiViewWs: Selected containers: ${state.selectedContainers.join(', ')}`);
// Закрываем существующее соединение
closeWs(containerId);
// Закрываем существующее соединение только если оно действительно существует
const existingConnection = state.open[containerId];
if (existingConnection && existingConnection.ws) {
console.log(`openMultiViewWs: Closing existing connection for ${service.name} (${containerId})`);
closeWs(containerId);
// Добавляем небольшую задержку перед созданием нового соединения
setTimeout(() => {
createWebSocketConnection(service, containerId);
}, 100);
return;
}
// Создаем новое WebSocket соединение
createWebSocketConnection(service, containerId);
}
function createWebSocketConnection(service, containerId) {
console.log(`createWebSocketConnection: Creating WebSocket for ${service.name} (${containerId})`);
const ws = new WebSocket(wsUrl(containerId, service.service, service.project));
ws.onopen = () => {
@@ -2090,6 +2132,9 @@ function openMultiViewWs(service) {
allLogs: [] // Добавляем буфер для логов
};
// Также сохраняем WebSocket в wsConnections для проверки в applyPanelOrder
state.wsConnections[containerId] = ws;
console.log(`openMultiViewWs: WebSocket setup completed for ${service.name} (${containerId})`);
}
@@ -2189,6 +2234,14 @@ async function fetchServices(){
buildTabs();
// Проверяем, нужно ли пропустить восстановление (например, после автоматического обновления)
const skipRestore = localStorage.getItem('lb_skip_restore');
if (skipRestore === 'true') {
console.log('Skipping panel restoration due to lb_skip_restore flag');
localStorage.removeItem('lb_skip_restore');
return;
}
// Восстанавливаем режим просмотра из localStorage
const savedViewMode = getViewModeFromStorage();
if (savedViewMode) {
@@ -2214,6 +2267,56 @@ async function fetchServices(){
// Настраиваем Multi View
await setupMultiView();
// Проверяем настройку автоматического обновления логов при восстановлении панелей
const autoRefreshOnRestore = localStorage.getItem('lb_auto_refresh_on_restore');
if (autoRefreshOnRestore === 'true') {
console.log('Auto-refresh logs on restore is enabled, refreshing logs in 1 second...');
setTimeout(() => {
// Обновляем логи панелей вместо обновления страницы
refreshLogsAndCounters();
console.log('Logs refreshed after panel restoration');
// Дополнительная прокрутка через небольшую задержку
setTimeout(() => {
scrollToBottom();
}, 1500);
}, 1000);
} else {
// Если это восстановление из localStorage, проверяем через некоторое время
// нужно ли обновить страницу для корректной работы обработчиков
setTimeout(() => {
const hasLevelButtons = document.querySelectorAll('.level-btn').length > 0;
if (hasLevelButtons) {
console.log('Panel restoration completed, checking event handlers in 2 seconds...');
setTimeout(() => {
// Простая проверка - если кнопки есть, но клики не работают, обновляем страницу
const testButton = document.querySelector('.level-btn');
if (testButton) {
// Симулируем клик для проверки
const clickEvent = new MouseEvent('click', { bubbles: true });
const originalHandler = testButton.onclick;
// Временно устанавливаем обработчик для проверки
testButton.onclick = () => {
console.log('Test click handler works');
testButton.onclick = originalHandler;
};
testButton.dispatchEvent(clickEvent);
// Если через 100ms обработчик не сработал, обновляем логи
setTimeout(() => {
if (testButton.onclick && testButton.onclick.toString().includes('Test click handler works')) {
console.log('Event handlers not working after restoration, refreshing logs...');
refreshLogsAndCounters();
}
}, 100);
}
}, 2000);
}
}, 1000);
}
} else if (savedViewMode.selectedContainers.length === 1) {
// Восстанавливаем Single View режим
console.log('Restoring Single View mode for container:', savedViewMode.selectedContainers[0]);
@@ -3947,6 +4050,27 @@ function updateLogLevelsVisibility() {
}
// Функция для обновления логов и счетчиков
/**
* Автоматически прокручивает логи к самому низу (последние логи)
* Работает как для single-view, так и для multi-view режимов
*/
function scrollToBottom() {
if (state.multiViewMode && state.selectedContainers.length > 0) {
// Для multi-view прокручиваем все панели
state.selectedContainers.forEach(containerId => {
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
if (multiViewLog) {
multiViewLog.scrollTop = multiViewLog.scrollHeight;
console.log(`Scrolled to bottom for container ${containerId}`);
}
});
} else if (els.logContent) {
// Для single-view прокручиваем основной контент
els.logContent.scrollTop = els.logContent.scrollHeight;
console.log('Scrolled to bottom for single view');
}
}
async function refreshLogsAndCounters() {
if (state.multiViewMode && state.selectedContainers.length > 0) {
// Обновляем мультипросмотр
@@ -3979,6 +4103,8 @@ async function refreshLogsAndCounters() {
recalculateMultiViewCounters();
// Применяем настройки wrap text после обновления
applyWrapSettings();
// Прокручиваем к последним логам
scrollToBottom();
}, 1000); // Небольшая задержка для завершения переподключения
} else if (state.current) {
@@ -4012,6 +4138,8 @@ async function refreshLogsAndCounters() {
recalculateCounters();
// Применяем настройки wrap text после обновления
applyWrapSettings();
// Прокручиваем к последним логам
scrollToBottom();
}, 1000); // Небольшая задержка для завершения переподключения
} else {
@@ -5020,6 +5148,7 @@ function reinitializeElements() {
els.sidebar = document.getElementById('sidebar');
els.sidebarToggle = document.getElementById('sidebarToggle');
els.header = document.getElementById('header');
els.autoRefreshOnRestore = document.getElementById('autoRefreshOnRestore');
console.log('Elements reinitialized:', {
filter: !!els.filter,
@@ -5282,13 +5411,40 @@ function reinitializeElements() {
});
// Обработчики для кнопок уровней логирования в заголовках
document.addEventListener('click', (e) => {
if (e.target.closest('.level-btn')) {
const levelBtn = e.target.closest('.level-btn');
console.log('Setting up level button click handler...');
// Удаляем предыдущий обработчик, если он существует
if (window.levelButtonClickHandler) {
document.removeEventListener('click', window.levelButtonClickHandler);
console.log('Removed previous level button click handler');
}
// Создаем новый обработчик
window.levelButtonClickHandler = (e) => {
console.log('Document click event:', e.target);
console.log('Event target closest level-btn:', e.target.closest('.level-btn'));
// Проверяем, что клик произошел на кнопке уровня логирования или на ее дочернем элементе
let levelBtn = null;
// Если клик произошел на самой кнопке
if (e.target.classList.contains('level-btn')) {
levelBtn = e.target;
console.log('Click on button itself');
}
// Если клик произошел на дочернем элементе кнопки
else if (e.target.closest('.level-btn')) {
levelBtn = e.target.closest('.level-btn');
console.log('Click on child element of button');
}
if (levelBtn) {
const level = levelBtn.getAttribute('data-level');
const containerId = levelBtn.getAttribute('data-container-id');
console.log(`Клик по кнопке уровня логирования: level=${level}, containerId=${containerId}, multiViewMode=${state.multiViewMode}`);
console.log(`Кнопка найдена:`, levelBtn);
console.log(`Текущие классы кнопки:`, levelBtn.className);
// Переключаем состояние кнопки
const isActive = levelBtn.classList.contains('active');
@@ -5341,7 +5497,7 @@ function reinitializeElements() {
}, 100);
}
}
});
};
// Добавляем тестовые функции в глобальную область для отладки
window.testDuplicateRemoval = testDuplicateRemoval;
@@ -5765,6 +5921,22 @@ function reinitializeElements() {
checkbox.checked = true;
ajaxUpdateEnabled = true;
// Инициализируем чекбокс автообновления при восстановлении панелей
const autoRefreshCheckbox = els.autoRefreshOnRestore;
if (autoRefreshCheckbox) {
// Восстанавливаем состояние из localStorage
const savedState = localStorage.getItem('lb_auto_refresh_on_restore');
autoRefreshCheckbox.checked = savedState === 'true';
// Добавляем обработчик изменения
autoRefreshCheckbox.addEventListener('change', function() {
localStorage.setItem('lb_auto_refresh_on_restore', this.checked ? 'true' : 'false');
console.log('Auto-refresh on restore setting changed:', this.checked);
});
autoRefreshCheckbox.title = 'Автоматически обновлять логи панелей при восстановлении из localStorage';
}
// Обновляем видимость кнопки refresh и состояние кнопки update при инициализации
updateRefreshButtonVisibility();
@@ -6136,15 +6308,21 @@ function reinitializeElements() {
function loadPanelOrder() {
try {
const savedOrder = localStorage.getItem('lb_panel_order');
console.log('loadPanelOrder: Raw savedOrder from localStorage:', savedOrder);
if (savedOrder) {
const order = JSON.parse(savedOrder);
console.log('loadPanelOrder: Parsed order:', order);
// Удаляем дубликаты из загруженного порядка
const uniqueOrder = [...new Set(order)];
console.log('Panel order loaded:', uniqueOrder);
console.log('loadPanelOrder: Unique order:', uniqueOrder);
return uniqueOrder;
} else {
console.log('loadPanelOrder: No saved order found in localStorage');
}
} catch (error) {
console.error('Error loading panel order:', error);
console.error('loadPanelOrder: Error loading panel order:', error);
}
return null;
}
@@ -6153,13 +6331,29 @@ function reinitializeElements() {
* Применяет сохраненный порядок панелей
*/
function applyPanelOrder() {
if (!state.multiViewMode) return;
console.log('applyPanelOrder: Starting...');
console.log('applyPanelOrder: multiViewMode:', state.multiViewMode);
if (!state.multiViewMode) {
console.log('applyPanelOrder: Not in multiViewMode, exiting');
return;
}
const savedOrder = loadPanelOrder();
if (!savedOrder || savedOrder.length === 0) return;
console.log('applyPanelOrder: savedOrder:', savedOrder);
if (!savedOrder || savedOrder.length === 0) {
console.log('applyPanelOrder: No saved order found, exiting');
return;
}
const grid = document.getElementById('multiViewGrid');
if (!grid) return;
console.log('applyPanelOrder: grid element:', grid);
if (!grid) {
console.log('applyPanelOrder: Grid not found, exiting');
return;
}
// Создаем карту панелей по ID контейнера
const panels = Array.from(grid.children);
@@ -6176,6 +6370,19 @@ function reinitializeElements() {
const panel = panelMap[containerId];
if (panel && panel.parentNode === grid) {
grid.appendChild(panel);
// Убеждаемся, что WebSocket соединение установлено для переставленной панели
const service = state.services.find(s => s.id === containerId);
if (service) {
// Проверяем, есть ли уже WebSocket соединение
const existingWs = state.wsConnections && state.wsConnections[containerId];
if (!existingWs || existingWs.readyState !== WebSocket.OPEN) {
setTimeout(() => {
console.log(`Re-establishing WebSocket for reordered panel: ${service.name} (${containerId})`);
openMultiViewWs(service);
}, 100);
}
}
}
});
@@ -6192,6 +6399,12 @@ function reinitializeElements() {
console.log(`Creating new panel for container: ${service.name} (${containerId})`);
const panel = createMultiViewPanel(service);
grid.appendChild(panel);
// Создаем WebSocket соединение для новой панели
setTimeout(() => {
console.log(`Setting up WebSocket for new panel: ${service.name} (${containerId})`);
openMultiViewWs(service);
}, 100);
}
} else {
console.log(`Panel for container ${containerId} already exists, skipping creation`);
@@ -6216,6 +6429,52 @@ function reinitializeElements() {
console.log(`Updated grid template columns to: repeat(${columns}, 1fr) for ${totalPanels} panels`);
console.log('Applied panel order:', state.selectedContainers);
// Инициализируем кнопки уровней логирования для восстановленных панелей
setTimeout(() => {
console.log('applyPanelOrder: Initializing level buttons for restored panels');
initializeLevelButtons();
// Обновляем логи для восстановленных панелей
setTimeout(() => {
console.log('applyPanelOrder: Refreshing logs for restored panels');
refreshLogsAndCounters();
// Дополнительная прокрутка к последним логам
setTimeout(() => {
scrollToBottom();
}, 1300);
}, 300);
// Проверяем, работают ли обработчики событий корректно
// Если нет, обновляем страницу для полной инициализации
setTimeout(() => {
const testButton = document.querySelector('.level-btn');
if (testButton) {
console.log('applyPanelOrder: Testing event handlers functionality');
// Создаем тестовое событие для проверки
const testEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
// Проверяем, есть ли обработчик на кнопке
const hasHandler = testButton.onclick !== null ||
testButton.getAttribute('onclick') !== null ||
(window.levelButtonClickHandler && document.addEventListener);
if (!hasHandler) {
console.log('applyPanelOrder: Event handlers not working properly, refreshing logs');
// Обновляем логи панелей вместо обновления страницы
refreshLogsAndCounters();
} else {
console.log('applyPanelOrder: Event handlers working correctly');
}
}
}, 500);
}, 200);
}
/**