diff --git a/README.md b/README.md index a6a153d..4dfb018 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,75 @@ # LogBoard+ -**Автор:** Сергей Антропов -**Сайт:** https://devops.org.ru +Веб-интерфейс для просмотра логов Docker контейнеров в реальном времени. + +## Исправления дублирования строк и правильных переносов строк в режимах Single View и MultiView + +### Проблема +В режимах Single View и MultiView происходило дублирование строк логов и проблемы с переносами строк между логами, что затрудняло чтение и анализ логов. + +### Внесенные исправления + +#### Для режима Single View: + +1. **Обработка дублированных строк в WebSocket данных** + - Добавлена проверка на дублирование в функции `openWs` + - Используются только уникальные строки из входящих данных + +2. **Проверка дублирования при добавлении строк** + - Добавлена проверка на дублирование в функции `handleLine` для Single View + - Пропускаются дублированные строки перед добавлением в интерфейс + +3. **Правильные переносы строк** + - Добавлены переносы строк `\n` после каждой строки лога для читаемости + - Создана функция `processSingleViewSpecialReplacements()` для обработки строк + - Улучшена функция `cleanSingleViewEmptyLines()` для сохранения переносов строк + +4. **Универсальная функция очистки** + - Создана функция `cleanDuplicateLines()` для удаления последовательных дубликатов + - Работает как для Single View, так и для MultiView + +5. **Периодическая очистка** + - Добавлена очистка дублированных строк при обновлении счетчиков + - Очистка выполняется при обновлении логов и фильтров + +#### Для режима MultiView: + +1. **Предотвращение одновременного отображения в обычном и multi-view режимах** + - Логи теперь отображаются только в одном режиме одновременно + - Добавлена проверка `!state.multiViewMode` в функции `handleLine` + +2. **Обработка дублированных строк в WebSocket данных** + - Добавлена проверка на дублирование в функции `openMultiViewWs` + - Используются только уникальные строки из входящих данных + +3. **Функция очистки дублированных строк** + - Создана функция `cleanMultiViewDuplicateLines()` для удаления последовательных дубликатов + - Функция вызывается при добавлении новых строк и периодически + +4. **Улучшенная обработка специальных случаев** + - Улучшена функция `processMultiViewSpecialReplacements()` для обработки строк с "FoundINFO:" + - Добавлена проверка на дублирование в исходном тексте + +5. **Периодическая очистка** + - Добавлена периодическая очистка дублированных строк каждую секунду + - Очистка выполняется при обновлении логов и фильтров + +### Тестирование +Для тестирования исправлений используйте в консоли браузера: +```javascript +testDuplicateRemoval() // Тест функции очистки дубликатов MultiView +testSingleViewDuplicateRemoval() // Тест функции очистки дубликатов Single View +testSingleViewEmptyLinesRemoval() // Тест функции очистки пустых строк Single View +testSingleViewLineBreaks() // Тест правильного отображения переносов строк +checkMultiViewHTML() // Проверка HTML на наличие дубликатов +cleanDuplicateLines() // Универсальная функция очистки дубликатов +cleanSingleViewEmptyLines() // Функция очистки пустых строк +``` + +### Автор +Сергей Антропов +Сайт: https://devops.org.ru LogBoard+ — это веб-панель для просмотра логов микросервисов из `docker-compose` в **реальном времени** с поддержкой: - Вкладок по сервисам и репликам diff --git a/templates/index.html b/templates/index.html index 1fcf188..4e5f600 100644 --- a/templates/index.html +++ b/templates/index.html @@ -993,7 +993,7 @@ a{color:var(--link)} padding: 12px; font-size: 11px; line-height: 1.4; - white-space: pre-wrap; + white-space: pre; word-break: break-word; overflow: auto; background: var(--bg); @@ -1437,7 +1437,15 @@ function refreshAllLogs() { // Обновляем современный интерфейс if (state.current && state.current.id === id && els.logContent) { els.logContent.innerHTML = obj.logEl.innerHTML; + + // Очищаем дублированные строки в Single View после обновления + cleanDuplicateLines(els.logContent); + cleanSingleViewEmptyLines(els.logContent); } + + // Очищаем дублированные строки в legacy панели + cleanDuplicateLines(obj.logEl); + cleanSingleViewEmptyLines(obj.logEl); }); // Обновляем мультипросмотр @@ -1464,9 +1472,21 @@ function refreshAllLogs() { const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (multiViewLog) { multiViewLog.innerHTML = filteredHtml.join(''); + + // Очищаем дублированные строки после обновления + cleanMultiViewDuplicateLines(multiViewLog); } }); } + + // Пересчитываем счетчики в зависимости от режима после обновления логов + setTimeout(() => { + if (state.multiViewMode) { + recalculateMultiViewCounters(); + } else { + recalculateCounters(); + } + }, 100); } function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } @@ -2150,7 +2170,9 @@ async function setupMultiView() { console.log(`setupMultiView: Multi-view setup completed for ${state.selectedContainers.length} containers`); // Обновляем счетчики для multi view - await updateMultiViewCounters(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 1000); // Небольшая задержка для завершения загрузки логов } function createMultiViewPanel(service) { @@ -2165,7 +2187,7 @@ function createMultiViewPanel(service) {

${escapeHtml(service.name)}

-
Connecting...
+
`; @@ -2173,6 +2195,10 @@ function createMultiViewPanel(service) { const logElement = panel.querySelector(`.multi-view-log[data-container-id="${service.id}"]`); if (logElement) { console.log(`Multi-view log element created successfully for ${service.name}`); + // Очищаем пустые строки после создания панели + cleanMultiViewEmptyLines(logElement); + // Очищаем дублированные строки после создания панели + cleanMultiViewDuplicateLines(logElement); } else { console.error(`Failed to create multi-view log element for ${service.name}`); } @@ -2197,7 +2223,13 @@ function openMultiViewWs(service) { console.log(`Multi-view WebSocket connected for ${service.name}`); const logEl = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (logEl) { - logEl.textContent = 'Connected...'; + // Убираем сообщение "Connected..." для MultiView режима + logEl.textContent = ''; + // Очищаем пустые строки после установки соединения + setTimeout(() => { + cleanMultiViewEmptyLines(logEl); + cleanMultiViewDuplicateLines(logEl); + }, 100); } }; @@ -2206,12 +2238,50 @@ function openMultiViewWs(service) { const parts = (event.data||'').split(/\r?\n/); - for (let i=0;i line.trim().length > 0); + const uniqueLines = [...new Set(lines)]; + if (lines.length !== uniqueLines.length) { + console.log('🚨 WebSocket: ОБНАРУЖЕНО ДУБЛИРОВАНИЕ строк!'); + console.log('🚨 WebSocket: Всего строк:', lines.length); + console.log('🚨 WebSocket: Уникальных строк:', uniqueLines.length); + console.log('🚨 WebSocket: Дублированные строки:', lines.filter((line, index) => lines.indexOf(line) !== index)); + + // Используем только уникальные строки + const uniqueParts = uniqueLines.map(line => line.trim()).filter(line => line.length > 0); + + for (let i=0;i setWsState(Object.keys(state.open).length? 'on':'off'); ws.onerror = ()=> setWsState('err'); @@ -2416,13 +2490,52 @@ function openWs(svc, panel){ const parts = (ev.data||'').split(/\r?\n/); console.log(`openWs: Processing ${parts.length} lines for container ${id}`); - for (let i=0;i line.trim().length > 0); + const uniqueLines = [...new Set(lines)]; + if (lines.length !== uniqueLines.length) { + console.log('🚨 Single View WebSocket: ОБНАРУЖЕНО ДУБЛИРОВАНИЕ строк!'); + console.log('🚨 Single View WebSocket: Всего строк:', lines.length); + console.log('🚨 Single View WebSocket: Уникальных строк:', uniqueLines.length); + console.log('🚨 Single View WebSocket: Дублированные строки:', lines.filter((line, index) => lines.indexOf(line) !== index)); + + // Используем только уникальные строки + const uniqueParts = uniqueLines.map(line => line.trim()).filter(line => line.length > 0); + + for (let i=0;i { + const textContent = line.textContent || line.innerText || ''; + if (textContent.trim() === '') { + line.remove(); + } + }); + + // Также удаляем пустые текстовые узлы + const walker = document.createTreeWalker( + multiViewLog, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodesToRemove = []; + let node; + while (node = walker.nextNode()) { + if (node.textContent.trim() === '') { + textNodesToRemove.push(node); + } + } + + textNodesToRemove.forEach(node => node.remove()); +} + +/** + * Функция для очистки дублированных строк в multi view + * Удаляет последовательные дублированные строки + * @param {HTMLElement} multiViewLog - элемент лога multi view + */ +function cleanMultiViewDuplicateLines(multiViewLog) { + if (!multiViewLog) return; + + const lines = Array.from(multiViewLog.querySelectorAll('.line')); + let removedCount = 0; + + // Проходим по строкам с конца, чтобы не нарушить индексы + for (let i = lines.length - 1; i > 0; i--) { + const currentLine = lines[i]; + const previousLine = lines[i - 1]; + + if (currentLine && previousLine) { + const currentText = currentLine.textContent || currentLine.innerText || ''; + const previousText = previousLine.textContent || previousLine.innerText || ''; + + if (currentText.trim() === previousText.trim() && currentText.trim() !== '') { + console.log(`cleanMultiViewDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); + currentLine.remove(); + removedCount++; + } + } + } + + if (removedCount > 0) { + console.log(`cleanMultiViewDuplicateLines: Удалено ${removedCount} дублированных строк`); + } +} + +/** + * Универсальная функция для очистки дублированных строк + * Работает как для Single View, так и для MultiView + * @param {HTMLElement} logElement - элемент лога (любого типа) + */ +function cleanDuplicateLines(logElement) { + if (!logElement) return; + + const lines = Array.from(logElement.querySelectorAll('.line')); + let removedCount = 0; + + // Проходим по строкам с конца, чтобы не нарушить индексы + for (let i = lines.length - 1; i > 0; i--) { + const currentLine = lines[i]; + const previousLine = lines[i - 1]; + + if (currentLine && previousLine) { + const currentText = currentLine.textContent || currentLine.innerText || ''; + const previousText = previousLine.textContent || previousLine.innerText || ''; + + if (currentText.trim() === previousText.trim() && currentText.trim() !== '') { + console.log(`cleanDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); + currentLine.remove(); + removedCount++; + } + } + } + + if (removedCount > 0) { + console.log(`cleanDuplicateLines: Удалено ${removedCount} дублированных строк`); + } +} + +/** + * Функция для очистки лишних пустых строк в Single View + * Удаляет только лишние пустые строки, сохраняя переносы строк между логами + * @param {HTMLElement} logElement - элемент лога Single View + */ +function cleanSingleViewEmptyLines(logElement) { + if (!logElement) return; + + // Находим все строки в логе + const lines = Array.from(logElement.querySelectorAll('.line')); + let removedCount = 0; + + // Удаляем только полностью пустые строки (строки без текста) + lines.forEach(line => { + const textContent = line.textContent || line.innerText || ''; + if (textContent.trim() === '' && textContent.length === 0) { + line.remove(); + removedCount++; + } + }); + + // Удаляем только полностью пустые текстовые узлы (не переносы строк) + const walker = document.createTreeWalker( + logElement, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodesToRemove = []; + let node; + while (node = walker.nextNode()) { + // Удаляем только узлы, которые содержат только пробелы и переносы строк + if (node.textContent.trim() === '' && node.textContent.length > 0) { + // Проверяем, что это не просто перенос строки + const content = node.textContent; + if (content === '\n' || content === '\r\n') { + // Оставляем переносы строк + continue; + } + // Удаляем только если это множественные пробелы или табуляция + if (content.match(/^[\s\t]+$/)) { + textNodesToRemove.push(node); + } + } + } + + textNodesToRemove.forEach(node => node.remove()); + + if (removedCount > 0) { + console.log(`cleanSingleViewEmptyLines: Удалено ${removedCount} пустых строк`); + } +} + +/** + * Функция для нормализации пробелов в логах + * Заменяет множественные пробелы на один пробел + * @param {string} text - исходный текст + * @returns {string} - текст с нормализованными пробелами + */ +function normalizeSpaces(text) { + if (!text) return text; + + // Заменяем множественные пробелы на один пробел + // Используем регулярное выражение для замены 2+ пробелов на один + return text.replace(/\s{2,}/g, ' '); +} + +/** + * Функция для обработки специальных замен в MultiView логах + * Выполняет специфичные замены для улучшения читаемости логов + * @param {string} text - исходный текст + * @returns {string} - текст с примененными заменами + */ +function processMultiViewSpecialReplacements(text) { + if (!text) return text; + + let processedText = text; + + // Добавляем отладочную информацию для проверки + if (text.includes('FoundINFO:')) { + console.log('🔍 processMultiViewSpecialReplacements: Найдена строка с FoundINFO:', text); + } + + // Проверяем на дублирование строк в исходном тексте + const lines = processedText.split('\n'); + const uniqueLines = [...new Set(lines)]; + if (lines.length !== uniqueLines.length) { + console.log('🚨 processMultiViewSpecialReplacements: Обнаружено дублирование в исходном тексте!'); + console.log('🚨 Исходные строки:', lines.length, 'Уникальные строки:', uniqueLines.length); + // Убираем дублированные строки + processedText = uniqueLines.join('\n'); + } + + // Заменяем случаи, где INFO: прилипает к предыдущему тексту + // Ищем паттерн: любой текст + INFO: (но не в начале строки) + // Используем более точное регулярное выражение для поиска + processedText = processedText.replace(/([A-Za-z0-9\s]+)INFO:/g, '$1\nINFO:'); + + // Убираем лишние переносы строк в начале, если они есть + processedText = processedText.replace(/^\n+/, ''); + + // Проверяем результат + if (text.includes('FoundINFO:') && processedText !== text) { + console.log('✅ processMultiViewSpecialReplacements: Замена выполнена:', processedText); + } else if (text.includes('FoundINFO:') && processedText === text) { + console.log('❌ processMultiViewSpecialReplacements: Замена НЕ выполнена для:', text); + } + + return processedText; +} + +/** + * Функция для обработки специальных замен в Single View логах + * Не добавляет лишние переносы строк + * @param {string} text - исходный текст + * @returns {string} - текст с примененными заменами + */ +function processSingleViewSpecialReplacements(text) { + if (!text) return text; + + let processedText = text; + + // Добавляем отладочную информацию для проверки + if (text.includes('FoundINFO:')) { + console.log('🔍 processSingleViewSpecialReplacements: Найдена строка с FoundINFO:', text); + } + + // Проверяем на дублирование строк в исходном тексте + const lines = processedText.split('\n'); + const uniqueLines = [...new Set(lines)]; + if (lines.length !== uniqueLines.length) { + console.log('🚨 processSingleViewSpecialReplacements: Обнаружено дублирование в исходном тексте!'); + console.log('🚨 Исходные строки:', lines.length, 'Уникальные строки:', uniqueLines.length); + // Убираем дублированные строки + processedText = uniqueLines.join('\n'); + } + + // Для Single View НЕ добавляем переносы строк, только убираем дубликаты + // Убираем лишние переносы строк в начале, если они есть + processedText = processedText.replace(/^\n+/, ''); + + // Проверяем результат + if (text.includes('FoundINFO:') && processedText !== text) { + console.log('✅ processSingleViewSpecialReplacements: Замена выполнена:', processedText); + } else if (text.includes('FoundINFO:') && processedText === text) { + console.log('❌ processSingleViewSpecialReplacements: Замена НЕ выполнена для:', text); + } + + return processedText; +} + +// Тестовая функция для проверки работы processMultiViewLineBreaks +function testMultiViewLineBreaks() { + console.log('=== Тест функции processMultiViewLineBreaks ==='); + console.log('Тест 1 (1 символ):', JSON.stringify(processMultiViewLineBreaks('a'))); + console.log('Тест 2 (3 символа):', JSON.stringify(processMultiViewLineBreaks('abc'))); + console.log('Тест 3 (5 символов):', JSON.stringify(processMultiViewLineBreaks('abcde'))); + console.log('Тест 4 (6 символов):', JSON.stringify(processMultiViewLineBreaks('abcdef'))); + console.log('Тест 5 (с переносами):', JSON.stringify(processMultiViewLineBreaks('a\nb\nc'))); + console.log('Тест 6 (длинная строка):', JSON.stringify(processMultiViewLineBreaks('Это длинная строка с текстом'))); + console.log('=== Конец теста ==='); +} + +// Функция для тестирования исправлений дублирования +function testDuplicateRemoval() { + console.log('=== Тест исправлений дублирования ==='); + + // Создаем тестовый элемент + const testElement = document.createElement('div'); + testElement.innerHTML = ` + Первая строка + Вторая строка + Вторая строка + Третья строка + Третья строка + Четвертая строка + `; + + console.log('До очистки дубликатов:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML до очистки:', testElement.innerHTML); + + cleanMultiViewDuplicateLines(testElement); + + console.log('После очистки дубликатов:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML после очистки:', testElement.innerHTML); + + console.log('=== Конец теста дублирования ==='); +} + +// Функция для тестирования Single View дублирования +function testSingleViewDuplicateRemoval() { + console.log('=== Тест Single View дублирования ==='); + + // Создаем тестовый элемент для Single View + const testElement = document.createElement('div'); + testElement.innerHTML = ` + INFO: 172.22.0.1:59030 - "GET /api/logs/stats/b816b3f326b9 HTTP/1.1" 200 OK + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 401 Unauthorized + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 401 Unauthorized + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 200 OK + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 200 OK + INFO: 172.22.0.1:61392 - "GET /api/projects HTTP/1.1" 200 OK + `; + + console.log('До очистки Single View дубликатов:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML до очистки:', testElement.innerHTML); + + cleanDuplicateLines(testElement); + + console.log('После очистки Single View дубликатов:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML после очистки:', testElement.innerHTML); + + console.log('=== Конец теста Single View дублирования ==='); +} + +// Функция для тестирования очистки пустых строк в Single View +function testSingleViewEmptyLinesRemoval() { + console.log('=== Тест очистки пустых строк в Single View ==='); + + // Создаем тестовый элемент с пустыми строками + const testElement = document.createElement('div'); + testElement.innerHTML = ` + INFO: 172.22.0.1:59030 - "GET /api/logs/stats/b816b3f326b9 HTTP/1.1" 200 OK + + + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 401 Unauthorized + + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 200 OK + + INFO: 172.22.0.1:61392 - "GET /api/projects HTTP/1.1" 200 OK + `; + + console.log('До очистки пустых строк:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML до очистки:', testElement.innerHTML); + + cleanSingleViewEmptyLines(testElement); + + console.log('После очистки пустых строк:', testElement.querySelectorAll('.line').length, 'строк'); + console.log('HTML после очистки:', testElement.innerHTML); + + console.log('=== Конец теста очистки пустых строк ==='); +} + +// Функция для тестирования правильного отображения переносов строк +function testSingleViewLineBreaks() { + console.log('=== Тест правильного отображения переносов строк в Single View ==='); + + // Создаем тестовый элемент с правильными переносами строк + const testElement = document.createElement('div'); + testElement.innerHTML = ` + INFO: 172.22.0.1:59030 - "GET /api/logs/stats/b816b3f326b9 HTTP/1.1" 200 OK\n + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 401 Unauthorized\n + INFO: 172.22.0.1:61392 - "GET / HTTP/1.1" 200 OK\n + INFO: 172.22.0.1:61392 - "GET /api/projects HTTP/1.1" 200 OK\n + `; + + console.log('Тестовый элемент с переносами строк:'); + console.log('Количество строк:', testElement.querySelectorAll('.line').length); + console.log('HTML:', testElement.innerHTML); + console.log('Текстовое содержимое:', testElement.textContent); + + // Проверяем, что переносы строк присутствуют + const textContent = testElement.textContent; + const lineBreaks = (textContent.match(/\n/g) || []).length; + console.log('Количество переносов строк в тексте:', lineBreaks); + + console.log('=== Конец теста переносов строк ==='); +} + +// Тестовая функция для проверки работы cleanMultiViewEmptyLines +function testCleanMultiViewEmptyLines() { + console.log('=== Тест функции cleanMultiViewEmptyLines ==='); + + // Создаем тестовый элемент + const testElement = document.createElement('div'); + testElement.innerHTML = ` + Первая строка лога + + + Вторая строка лога + + Третья строка лога + `; + + console.log('До очистки:', testElement.innerHTML); + cleanMultiViewEmptyLines(testElement); + console.log('После очистки:', testElement.innerHTML); + console.log('=== Конец теста ==='); +} + +// Тестовая функция для проверки работы normalizeSpaces +function testNormalizeSpaces() { + console.log('=== Тест функции normalizeSpaces ==='); + console.log('Тест 1 (обычная строка):', JSON.stringify(normalizeSpaces('Hello World'))); + console.log('Тест 2 (двойные пробелы):', JSON.stringify(normalizeSpaces('Hello World'))); + console.log('Тест 3 (множественные пробелы):', JSON.stringify(normalizeSpaces('Hello World'))); + console.log('Тест 4 (пробелы в начале и конце):', JSON.stringify(normalizeSpaces(' Hello World '))); + console.log('Тест 5 (табуляция и пробелы):', JSON.stringify(normalizeSpaces('Hello\t\tWorld'))); + console.log('Тест 6 (смешанные пробелы):', JSON.stringify(normalizeSpaces('Hello \t World'))); + console.log('Тест 7 (пустая строка):', JSON.stringify(normalizeSpaces(''))); + console.log('Тест 8 (null):', JSON.stringify(normalizeSpaces(null))); + console.log('=== Конец теста ==='); +} + +// Тестовая функция для проверки работы processMultiViewSpecialReplacements +function testMultiViewSpecialReplacements() { + console.log('=== Тест функции processMultiViewSpecialReplacements ==='); + console.log('Тест 1 (обычная строка):', JSON.stringify(processMultiViewSpecialReplacements('Hello World'))); + console.log('Тест 2 (200 OKINFO:):', JSON.stringify(processMultiViewSpecialReplacements('200 OKINFO: Some message'))); + console.log('Тест 3 (404 Not FoundINFO:):', JSON.stringify(processMultiViewSpecialReplacements('404 Not FoundINFO: Some message'))); + console.log('Тест 4 (FoundINFO:):', JSON.stringify(processMultiViewSpecialReplacements('FoundINFO: Some message'))); + console.log('Тест 5 (Found INFO:):', JSON.stringify(processMultiViewSpecialReplacements('Found INFO: Some message'))); + console.log('Тест 6 (500 Internal Server ErrorINFO:):', JSON.stringify(processMultiViewSpecialReplacements('500 Internal Server ErrorINFO: Some message'))); + console.log('Тест 7 (GET /api/usersINFO:):', JSON.stringify(processMultiViewSpecialReplacements('GET /api/usersINFO: Some message'))); + console.log('Тест 8 (POST /api/loginINFO:):', JSON.stringify(processMultiViewSpecialReplacements('POST /api/loginINFO: Some message'))); + console.log('Тест 9 (INFO: в начале):', JSON.stringify(processMultiViewSpecialReplacements('INFO: Some message'))); + console.log('Тест 10 (несколько INFO:):', JSON.stringify(processMultiViewSpecialReplacements('200 OKINFO: First INFO: Second'))); + console.log('Тест 11 (пустая строка):', JSON.stringify(processMultiViewSpecialReplacements(''))); + console.log('Тест 12 (null):', JSON.stringify(processMultiViewSpecialReplacements(null))); + console.log('=== Конец теста ==='); +} + +// Комплексная тестовая функция для проверки полного процесса обработки MultiView +function testFullMultiViewProcessing() { + console.log('=== Тест полного процесса обработки MultiView ==='); + + const testCases = [ + '200 OKINFO: Some message', + '404 Not FoundINFO: Another message', + '500 Internal Server ErrorINFO: Third message', + 'FoundINFO: First FoundINFO: Second', + 'GET /api/usersINFO: API call', + 'POST /api/loginINFO: Login attempt', + 'Short', + 'Long message with 200 OKINFO: inside' + ]; + + testCases.forEach((testCase, index) => { + console.log(`\nТест ${index + 1}: "${testCase}"`); + + // 1. Нормализация пробелов + const normalized = normalizeSpaces(testCase); + console.log(' 1. Нормализация пробелов:', JSON.stringify(normalized)); + + // 2. Специальные замены + const specialProcessed = processMultiViewSpecialReplacements(normalized); + console.log(' 2. Специальные замены:', JSON.stringify(specialProcessed)); + + // 3. Обработка переноса строк + const finalProcessed = processMultiViewLineBreaks(specialProcessed); + console.log(' 3. Перенос строк:', JSON.stringify(finalProcessed)); + + console.log(' Результат:', finalProcessed); + }); + + console.log('\n=== Конец комплексного теста ==='); +} + +// Быстрая функция для тестирования замены INFO: +function quickTestINFO() { + console.log('=== Быстрый тест замены INFO: ==='); + const testStrings = [ + 'INFO: 127.0.0.1:53352 - "GET /health HTTP/1.1" 200 OKINFO: 127.0.0.1:53352 - "GET /health HTTP/1.1" 200 OK', + 'INFO: 127.0.0.1:48988 - "GET /health HTTP/1.1" 200 OKINFO: 127.0.0.1:48988 - "GET /health HTTP/1.1" 200 OK', + 'INFO: 127.0.0.1:44606 - "GET /health HTTP/1.1" 200 OKINFO: 127.0.0.1:44606 - "GET /health HTTP/1.1" 200 OK', + '200 OKINFO:', + '404 Not FoundINFO:', + '500 Internal Server ErrorINFO:', + 'FoundINFO:', + 'INFO:' + ]; + + testStrings.forEach((str, index) => { + const result = processMultiViewSpecialReplacements(str); + console.log(`Тест ${index + 1}: "${str}" -> "${result}"`); + }); + console.log('=== Конец быстрого теста ==='); +} + +// Функция для тестирования регулярного выражения +function testRegex() { + console.log('=== Тест регулярного выражения ==='); + + const testString = 'INFO: 172.22.0.10:33860 - "POST /api/v1/auth/verify HTTP/1.1" 404 Not FoundINFO: 172.22.0.10:33860 - "POST /api/v1/auth/verify HTTP/1.1" 404 Not Found'; + + console.log('Исходная строка:', testString); + + // Тестируем наше регулярное выражение + const result = testString.replace(/([A-Za-z0-9\s]+)INFO:/g, '$1\nINFO:'); + console.log('Результат замены:', result); + + // Проверяем, есть ли совпадения + const matches = testString.match(/([A-Za-z0-9\s]+)INFO:/g); + console.log('Найденные совпадения:', matches); + + console.log('=== Конец теста регулярного выражения ==='); +} + +// Функция для проверки HTML в MultiView на наличие FoundINFO: +function checkMultiViewHTML() { + console.log('=== Проверка HTML в MultiView ==='); + + const multiViewLogs = document.querySelectorAll('.multi-view-log'); + console.log('Найдено MultiView логов:', multiViewLogs.length); + + multiViewLogs.forEach((log, index) => { + const containerId = log.getAttribute('data-container-id'); + console.log(`MultiView ${index + 1} (${containerId}):`); + + // Проверяем весь HTML + const html = log.innerHTML; + if (html.includes('FoundINFO:')) { + console.log('🚨 НАЙДЕНО FoundINFO: в HTML!'); + console.log('HTML:', html); + } else { + console.log('✅ FoundINFO: не найдено в HTML'); + } + + // Проверяем текстовое содержимое + const textContent = log.textContent; + if (textContent.includes('FoundINFO:')) { + console.log('🚨 НАЙДЕНО FoundINFO: в тексте!'); + console.log('Текст:', textContent); + } else { + console.log('✅ FoundINFO: не найдено в тексте'); + } + }); + + console.log('=== Конец проверки HTML ==='); +} + // Глобальная функция для обработки логов function handleLine(id, line){ - console.log(`handleLine: Called for container ${id}, line: "${line.substring(0, 50)}..."`); const obj = state.open[id]; if (!obj) { @@ -2451,33 +3116,66 @@ function handleLine(id, line){ obj.counters = {dbg:0, info:0, warn:0, err:0}; } - const cls = classify(line); + // Фильтруем сообщение "Connected to container" для всех режимов + // Это сообщение отправляется сервером при установке WebSocket соединения + if (line.includes('Connected to container:')) { + console.log(`handleLine: Фильтруем сообщение "Connected to container" для контейнера ${id}`); + return; // Пропускаем это сообщение во всех режимах + } - // Обновляем счетчики - if (obj.counters) { + // Нормализуем пробелы в строке лога + const normalizedLine = normalizeSpaces(line); + + const cls = classify(normalizedLine); + + // Обновляем счетчики только для отображаемых логов + // Проверяем фильтры для отображения + const shouldShow = allowedByLevel(cls) && applyFilter(normalizedLine); + + // Обновляем счетчики только если строка будет отображаться + if (obj.counters && shouldShow) { if (cls==='dbg') obj.counters.dbg++; if (cls==='ok') obj.counters.info++; if (cls==='warn') obj.counters.warn++; if (cls==='err') obj.counters.err++; } - const html = `${ansiToHtml(line)}\n`; + // Для Single View добавляем перенос строки после каждой строки лога + const html = `${ansiToHtml(normalizedLine)}\n`; // Сохраняем все логи в буфере (всегда) if (!obj.allLogs) obj.allLogs = []; - obj.allLogs.push({html: html, line: line, cls: cls}); + // Для Single View сохраняем обработанную строку, для MultiView - оригинальную + const processedLine = !state.multiViewMode ? processSingleViewSpecialReplacements(normalizedLine) : normalizedLine; + const processedHtml = `${ansiToHtml(processedLine)}\n`; + obj.allLogs.push({html: processedHtml, line: processedLine, cls: cls}); // Ограничиваем размер буфера if (obj.allLogs.length > 10000) { obj.allLogs = obj.allLogs.slice(-5000); } - // Проверяем фильтры для отображения - const shouldShow = allowedByLevel(cls) && applyFilter(line); - - // Добавляем логи в отображение (обычный просмотр) - if (shouldShow && obj.logEl) { - obj.logEl.insertAdjacentHTML('beforeend', html); + // Добавляем логи в отображение (обычный просмотр) - только если НЕ в multi-view режиме + if (shouldShow && obj.logEl && !state.multiViewMode) { + // Обрабатываем строку для Single View (без лишних переносов строк) + const singleViewProcessedLine = processSingleViewSpecialReplacements(normalizedLine); + + // Проверяем на дублирование в Single View логах + const existingLines = Array.from(obj.logEl.querySelectorAll('.line')); + const lastLine = existingLines[existingLines.length - 1]; + if (lastLine && lastLine.textContent === singleViewProcessedLine) { + console.log(`handleLine: Пропускаем дублированную строку для Single View контейнера ${id}:`, singleViewProcessedLine.substring(0, 50)); + return; // Пропускаем дублированную строку + } + + // Создаем HTML с обработанной строкой для Single View (с переносом строки) + const singleViewHtml = `${ansiToHtml(singleViewProcessedLine)}\n`; + + obj.logEl.insertAdjacentHTML('beforeend', singleViewHtml); + + // Очищаем лишние пустые строки в Single View + cleanSingleViewEmptyLines(obj.logEl); + if (els.autoscroll && els.autoscroll.checked && obj.wrapEl) { obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; } @@ -2485,9 +3183,13 @@ function handleLine(id, line){ // Update modern interface console.log(`handleLine: Checking modern interface update - state.current:`, state.current, `id:`, id, `els.logContent:`, !!els.logContent); if (state.current && state.current.id === id && els.logContent) { - console.log(`handleLine: Updating modern interface for container ${id} with html:`, html.substring(0, 100)); + console.log(`handleLine: Updating modern interface for container ${id} with html:`, singleViewHtml.substring(0, 100)); // Добавляем новую строку напрямую в современный интерфейс - els.logContent.insertAdjacentHTML('beforeend', html); + els.logContent.insertAdjacentHTML('beforeend', singleViewHtml); + + // Очищаем лишние пустые строки в современном интерфейсе + cleanSingleViewEmptyLines(els.logContent); + console.log(`handleLine: Modern interface updated, logContent children count:`, els.logContent.children.length); if (els.autoscroll && els.autoscroll.checked) { els.logContent.scrollTop = els.logContent.scrollHeight; @@ -2505,8 +3207,35 @@ function handleLine(id, line){ // Применяем ограничение tail lines в multi view const tailLines = parseInt(els.tail.value) || 50; + // Порядок обработки строк для MultiView: + // 1. Нормализация пробелов (уже выполнена выше) + // 2. Специальные замены (например, "FoundINFO:" -> "Found\nINFO:") + // 3. Обработка переноса строк + const specialProcessedLine = processMultiViewSpecialReplacements(normalizedLine); + + // Обрабатываем перенос строк для multi view + // Если символов больше 5, то перенос строк работает + // Если меньше 5, то переноса строк нет + const processedLine = processMultiViewLineBreaks(specialProcessedLine); + + // Проверяем на дублирование в multi-view логах + const existingLines = Array.from(multiViewLog.querySelectorAll('.line')); + const lastLine = existingLines[existingLines.length - 1]; + if (lastLine && lastLine.textContent === processedLine) { + console.log(`handleLine: Пропускаем дублированную строку для контейнера ${id}:`, processedLine.substring(0, 50)); + return; // Пропускаем дублированную строку + } + + const multiViewHtml = `${ansiToHtml(processedLine)}\n`; + // Добавляем новую строку - multiViewLog.insertAdjacentHTML('beforeend', html); + multiViewLog.insertAdjacentHTML('beforeend', multiViewHtml); + + // Очищаем пустые строки в multi view + cleanMultiViewEmptyLines(multiViewLog); + + // Очищаем дублированные строки в multi view + cleanMultiViewDuplicateLines(multiViewLog); // Ограничиваем количество отображаемых строк const logLines = Array.from(multiViewLog.querySelectorAll('.line')); @@ -2534,6 +3263,15 @@ function handleLine(id, line){ if (!state.multiViewCounterUpdateTimer) { state.multiViewCounterUpdateTimer = setTimeout(() => { updateMultiViewCounters(); + + // Периодически очищаем дублированные строки во всех multi-view логах + state.selectedContainers.forEach(containerId => { + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); + if (multiViewLog) { + cleanMultiViewDuplicateLines(multiViewLog); + } + }); + state.multiViewCounterUpdateTimer = null; }, 1000); // Обновляем каждую секунду } @@ -2660,6 +3398,9 @@ async function switchToSingle(svc){ updateLogTitle(); // Обновляем счетчики для нового контейнера + setTimeout(() => { + recalculateCounters(); + }, 500); // Небольшая задержка для завершения загрузки логов await updateCounters(svc.id); // Добавляем обработчики для счетчиков после переключения контейнера @@ -2822,6 +3563,22 @@ async function updateCounters(containerId) { // Добавляем обработчики для счетчиков addCounterClickHandlers(); + + // Очищаем дублированные строки в Single View режиме + if (!state.multiViewMode && state.current && state.current.id === containerId) { + const logContent = document.querySelector('.log-content'); + if (logContent) { + cleanDuplicateLines(logContent); + cleanSingleViewEmptyLines(logContent); + } + + // Также очищаем в legacy панели + const obj = state.open[containerId]; + if (obj && obj.logEl) { + cleanDuplicateLines(obj.logEl); + cleanSingleViewEmptyLines(obj.logEl); + } + } } } catch (error) { console.error('Error updating counters:', error); @@ -2829,6 +3586,7 @@ async function updateCounters(containerId) { } // Функция для обновления счетчиков в multi view (суммирует статистику всех контейнеров) +// Эта функция теперь использует пересчет на основе отображаемых логов async function updateMultiViewCounters() { if (!state.multiViewMode || state.selectedContainers.length === 0) { return; @@ -2837,40 +3595,8 @@ async function updateMultiViewCounters() { try { console.log('Updating multi-view counters for containers:', state.selectedContainers); - // Суммируем статистику всех выбранных контейнеров - let totalDebug = 0; - let totalInfo = 0; - let totalWarn = 0; - let totalError = 0; - - // Получаем статистику для каждого контейнера - for (const containerId of state.selectedContainers) { - try { - const response = await fetch(`/api/logs/stats/${containerId}`); - if (response.ok) { - const stats = await response.json(); - totalDebug += stats.debug || 0; - totalInfo += stats.info || 0; - totalWarn += stats.warn || 0; - totalError += stats.error || 0; - } - } catch (error) { - console.error(`Error fetching stats for container ${containerId}:`, error); - } - } - - // Обновляем счетчики в интерфейсе - const cdbg = document.querySelector('.cdbg'); - const cinfo = document.querySelector('.cinfo'); - const cwarn = document.querySelector('.cwarn'); - const cerr = document.querySelector('.cerr'); - - if (cdbg) cdbg.textContent = totalDebug; - if (cinfo) cinfo.textContent = totalInfo; - if (cwarn) cwarn.textContent = totalWarn; - if (cerr) cerr.textContent = totalError; - - console.log('Multi-view counters updated:', { totalDebug, totalInfo, totalWarn, totalError }); + // Используем новую функцию пересчета счетчиков + recalculateMultiViewCounters(); // Обновляем видимость счетчиков updateCounterVisibility(); @@ -2883,6 +3609,108 @@ async function updateMultiViewCounters() { } } +// Функция для пересчета счетчиков на основе отображаемых логов (Single View) +function recalculateCounters() { + if (!state.current) return; + + const containerId = state.current.id; + const obj = state.open[containerId]; + if (!obj || !obj.allLogs) return; + + // Получаем значение Tail Lines + const tailLines = parseInt(els.tail.value) || 50; + + // Берем только последние N логов в соответствии с Tail Lines + const visibleLogs = obj.allLogs.slice(-tailLines); + + // Сбрасываем счетчики + obj.counters = {dbg: 0, info: 0, warn: 0, err: 0}; + + // Пересчитываем счетчики только для отображаемых логов + visibleLogs.forEach(logEntry => { + const shouldShow = allowedByLevel(logEntry.cls) && applyFilter(logEntry.line); + if (shouldShow) { + if (logEntry.cls === 'dbg') obj.counters.dbg++; + if (logEntry.cls === 'ok') obj.counters.info++; + if (logEntry.cls === 'warn') obj.counters.warn++; + if (logEntry.cls === 'err') obj.counters.err++; + } + }); + + // Обновляем отображение счетчиков + const cdbg = document.querySelector('.cdbg'); + const cinfo = document.querySelector('.cinfo'); + const cwarn = document.querySelector('.cwarn'); + const cerr = document.querySelector('.cerr'); + + if (cdbg) cdbg.textContent = obj.counters.dbg; + if (cinfo) cinfo.textContent = obj.counters.info; + if (cwarn) cwarn.textContent = obj.counters.warn; + if (cerr) cerr.textContent = obj.counters.err; + + console.log(`Counters recalculated for container ${containerId} (tail: ${tailLines}):`, obj.counters); +} + +// Функция для пересчета счетчиков в MultiView на основе отображаемых логов +function recalculateMultiViewCounters() { + if (!state.multiViewMode || state.selectedContainers.length === 0) { + return; + } + + console.log('Recalculating multi-view counters for containers:', state.selectedContainers); + + // Получаем значение Tail Lines + const tailLines = parseInt(els.tail.value) || 50; + + // Суммируем статистику всех выбранных контейнеров + let totalDebug = 0; + let totalInfo = 0; + let totalWarn = 0; + let totalError = 0; + + // Пересчитываем счетчики для каждого контейнера + for (const containerId of state.selectedContainers) { + const obj = state.open[containerId]; + if (!obj || !obj.allLogs) continue; + + // Берем только последние N логов в соответствии с Tail Lines + const visibleLogs = obj.allLogs.slice(-tailLines); + + // Сбрасываем счетчики для этого контейнера + obj.counters = {dbg: 0, info: 0, warn: 0, err: 0}; + + // Пересчитываем счетчики только для отображаемых логов + visibleLogs.forEach(logEntry => { + const shouldShow = allowedByLevel(logEntry.cls) && applyFilter(logEntry.line); + if (shouldShow) { + if (logEntry.cls === 'dbg') obj.counters.dbg++; + if (logEntry.cls === 'ok') obj.counters.info++; + if (logEntry.cls === 'warn') obj.counters.warn++; + if (logEntry.cls === 'err') obj.counters.err++; + } + }); + + // Добавляем к общим счетчикам + totalDebug += obj.counters.dbg; + totalInfo += obj.counters.info; + totalWarn += obj.counters.warn; + totalError += obj.counters.err; + } + + // Обновляем отображение счетчиков + const cdbg = document.querySelector('.cdbg'); + const cinfo = document.querySelector('.cinfo'); + const cwarn = document.querySelector('.cwarn'); + const cerr = document.querySelector('.cerr'); + + if (cdbg) cdbg.textContent = totalDebug; + if (cinfo) cinfo.textContent = totalInfo; + if (cwarn) cwarn.textContent = totalWarn; + if (cerr) cerr.textContent = totalError; + + console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError }); +} + // Функция для обновления видимости счетчиков function updateCounterVisibility() { const debugBtn = document.querySelector('.debug-btn'); @@ -2910,8 +3738,8 @@ async function refreshLogsAndCounters() { // Обновляем мультипросмотр console.log('Refreshing multi-view for containers:', state.selectedContainers); - // Обновляем счетчики для всех выбранных контейнеров (суммируем статистику) - await updateMultiViewCounters(); + // Пересчитываем счетчики на основе отображаемых логов + recalculateMultiViewCounters(); // Перезапускаем WebSocket соединения для всех выбранных контейнеров state.selectedContainers.forEach(containerId => { @@ -2934,8 +3762,8 @@ async function refreshLogsAndCounters() { // Обычный режим просмотра console.log('Refreshing logs and counters for:', state.current.id); - // Обновляем счетчики - await updateCounters(state.current.id); + // Пересчитываем счетчики на основе отображаемых логов + recalculateCounters(); // Перезапускаем WebSocket соединение для получения свежих логов const currentId = state.current.id; @@ -2984,6 +3812,13 @@ els.clearBtn.onclick = ()=> { document.querySelectorAll('.cdbg, .cinfo, .cwarn, .cerr').forEach(el => { el.textContent = '0'; }); + + // Сбрасываем счетчики в объектах состояния + Object.values(state.open).forEach(obj => { + if (obj.counters) { + obj.counters = {dbg: 0, info: 0, warn: 0, err: 0}; + } + }); }; els.refreshBtn.onclick = async () => { @@ -3310,6 +4145,15 @@ if (els.tail) { if (state.current && els.logContent) { els.logContent.textContent = 'Reconnecting...'; } + + // Пересчитываем счетчики после изменения Tail Lines + setTimeout(() => { + if (state.multiViewMode) { + recalculateMultiViewCounters(); + } else { + recalculateCounters(); + } + }, 1000); // Небольшая задержка для завершения переподключения }; } if (els.wrapToggle) { @@ -3357,6 +4201,14 @@ if (els.filter) { els.filter.oninput = ()=> { state.filter = els.filter.value.trim(); refreshAllLogs(); + // Пересчитываем счетчики в зависимости от режима + setTimeout(() => { + if (state.multiViewMode) { + recalculateMultiViewCounters(); + } else { + recalculateCounters(); + } + }, 100); }; } // Обработчики для LogLevels (если элементы существуют) @@ -3368,6 +4220,14 @@ if (els.lvlDebug) { // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 100); + } else { + // Пересчитываем счетчики для Single View + setTimeout(() => { + recalculateCounters(); + }, 100); } }; } @@ -3379,6 +4239,14 @@ if (els.lvlInfo) { // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 100); + } else { + // Пересчитываем счетчики для Single View + setTimeout(() => { + recalculateCounters(); + }, 100); } }; } @@ -3390,6 +4258,14 @@ if (els.lvlWarn) { // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 100); + } else { + // Пересчитываем счетчики для Single View + setTimeout(() => { + recalculateCounters(); + }, 100); } }; } @@ -3401,6 +4277,14 @@ if (els.lvlErr) { // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 100); + } else { + // Пересчитываем счетчики для Single View + setTimeout(() => { + recalculateCounters(); + }, 100); } }; } @@ -3578,6 +4462,24 @@ window.addEventListener('keydown', async (e)=>{ } } }); + + // Добавляем тестовые функции в глобальную область для отладки + window.testDuplicateRemoval = testDuplicateRemoval; + window.testSingleViewDuplicateRemoval = testSingleViewDuplicateRemoval; + window.testSingleViewEmptyLinesRemoval = testSingleViewEmptyLinesRemoval; + window.testSingleViewLineBreaks = testSingleViewLineBreaks; + window.testMultiViewLineBreaks = testMultiViewLineBreaks; + window.testMultiViewSpecialReplacements = testMultiViewSpecialReplacements; + window.testFullMultiViewProcessing = testFullMultiViewProcessing; + window.quickTestINFO = quickTestINFO; + window.testRegex = testRegex; + window.checkMultiViewHTML = checkMultiViewHTML; + window.cleanMultiViewDuplicateLines = cleanMultiViewDuplicateLines; + window.cleanDuplicateLines = cleanDuplicateLines; + window.cleanSingleViewEmptyLines = cleanSingleViewEmptyLines; + + console.log('LogBoard+ инициализирован с исправлениями дублирования строк и правильными переносами строк в Single View и MultiView режимах'); + console.log('Для тестирования используйте: testDuplicateRemoval(), testSingleViewDuplicateRemoval(), testSingleViewEmptyLinesRemoval() или testSingleViewLineBreaks()'); })();