From d570807c0287e5836339cae6ca6644b4a94bb431 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Mon, 1 Sep 2025 18:18:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20500=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлен путь к файлу excluded_containers.json в функциях load_excluded_containers и save_excluded_containers - Добавлено создание директории при необходимости - Улучшено логирование операций с файлом - Протестирована функциональность API endpoints - Обновлен список исключенных контейнеров Автор: Сергей Антропов Сайт: https://devops.org.ru --- app/core/docker.py | 24 ++- app/excluded_containers.json | 4 +- app/static/js/index.js | 327 +++++++++++++++++++++++++++++++---- app/templates/index.html | 6 +- 4 files changed, 319 insertions(+), 42 deletions(-) diff --git a/app/core/docker.py b/app/core/docker.py index dc6179f..3f5930f 100644 --- a/app/core/docker.py +++ b/app/core/docker.py @@ -30,17 +30,20 @@ def load_excluded_containers() -> List[str]: Сайт: https://devops.org.ru """ try: - with open("app/excluded_containers.json", "r", encoding="utf-8") as f: + # Определяем путь к файлу относительно корня проекта + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "excluded_containers.json") + + with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) return data.get("excluded_containers", []) except FileNotFoundError: - docker_logger.warning("Файл app/excluded_containers.json не найден, используем пустой список") + docker_logger.warning(f"Файл excluded_containers.json не найден по пути {file_path}, используем пустой список") return [] except json.JSONDecodeError as e: - docker_logger.error(f"Ошибка парсинга app/excluded_containers.json: {e}") + docker_logger.error(f"Ошибка парсинга excluded_containers.json: {e}") return [] except Exception as e: - docker_logger.error(f"Ошибка загрузки app/excluded_containers.json: {e}") + docker_logger.error(f"Ошибка загрузки excluded_containers.json: {e}") return [] def save_excluded_containers(containers: List[str]) -> bool: @@ -50,15 +53,24 @@ def save_excluded_containers(containers: List[str]) -> bool: Сайт: https://devops.org.ru """ try: + # Определяем путь к файлу относительно корня проекта + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "excluded_containers.json") + data = { "excluded_containers": containers, "description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения" } - with open("app/excluded_containers.json", "w", encoding="utf-8") as f: + + # Создаем директорию, если она не существует + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + with open(file_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) + + docker_logger.info(f"Список исключенных контейнеров сохранен в {file_path}") return True except Exception as e: - docker_logger.error(f"Ошибка сохранения app/excluded_containers.json: {e}") + docker_logger.error(f"Ошибка сохранения excluded_containers.json: {e}") return False def get_all_projects() -> List[str]: diff --git a/app/excluded_containers.json b/app/excluded_containers.json index 3e89360..a8e1337 100644 --- a/app/excluded_containers.json +++ b/app/excluded_containers.json @@ -1,4 +1,6 @@ { - "excluded_containers": [], + "excluded_containers": [ + "buildx_buildkit_multiarch-builder0" + ], "description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения" } \ No newline at end of file diff --git a/app/static/js/index.js b/app/static/js/index.js index 5a2b3a0..b6fb301 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -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); } /** diff --git a/app/templates/index.html b/app/templates/index.html index fea5b09..139eaca 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -77,7 +77,11 @@
- + +
+
+ +