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)}
`;
@@ -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()');
})();