Исправлена проблема с лишними пустыми строками в Single View и Multi View

- Радикально переработаны функции очистки пустых строк
- Удалены переносы строк из HTML-шаблонов логов
- Добавлены CSS стили для правильного отображения элементов .line
- Увеличена частота периодической очистки до 2 секунд
- Добавлена агрессивная очистка во всех ключевых точках приложения
- Улучшена логика определения типа элемента для правильной очистки

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов 2025-08-17 18:02:43 +03:00
parent 012c31522c
commit 59e0810750

View File

@ -1098,9 +1098,9 @@ main{display:none}
.counter{font-size:11px;color:var(--muted)} .counter{font-size:11px;color:var(--muted)}
.logwrap{flex:1;overflow:auto;padding:10px} .logwrap{flex:1;overflow:auto;padding:10px}
.log{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;margin:0;tab-size:2} .log{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;margin:0;tab-size:2}
.line{color:var(--fg)} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .dbg{color:#7dcfff} .ts{color:var(--muted)} .line{color:var(--fg); display:block; margin:0; padding:0; line-height:1.4} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .dbg{color:#7dcfff} .ts{color:var(--muted)}
/* Цвета для multi-view логов */ /* Цвета для multi-view логов */
.multi-view-log .line{color:var(--fg) !important} .multi-view-log .line{color:var(--fg) !important; display:block; margin:0; padding:0; line-height:1.4}
.multi-view-log .ok{color:var(--ok) !important} .multi-view-log .ok{color:var(--ok) !important}
.multi-view-log .warn{color:var(--warn) !important} .multi-view-log .warn{color:var(--warn) !important}
.multi-view-log .err{color:var(--err) !important} .multi-view-log .err{color:var(--err) !important}
@ -1108,7 +1108,7 @@ main{display:none}
.multi-view-log .ts{color:var(--muted) !important} .multi-view-log .ts{color:var(--muted) !important}
/* Дополнительные стили для multi-view логов */ /* Дополнительные стили для multi-view логов */
.multi-view-log span.line{color:var(--fg) !important} .multi-view-log span.line{color:var(--fg) !important; display:block; margin:0; padding:0; line-height:1.4}
.multi-view-log span.ok{color:var(--ok) !important} .multi-view-log span.ok{color:var(--ok) !important}
.multi-view-log span.warn{color:var(--warn) !important} .multi-view-log span.warn{color:var(--warn) !important}
.multi-view-log span.err{color:var(--err) !important} .multi-view-log span.err{color:var(--err) !important}
@ -1434,18 +1434,18 @@ function refreshAllLogs() {
// Обновляем отображение // Обновляем отображение
obj.logEl.innerHTML = filteredHtml.join(''); obj.logEl.innerHTML = filteredHtml.join('');
// Сразу очищаем пустые строки в legacy панели
cleanSingleViewEmptyLines(obj.logEl);
cleanDuplicateLines(obj.logEl);
// Обновляем современный интерфейс // Обновляем современный интерфейс
if (state.current && state.current.id === id && els.logContent) { if (state.current && state.current.id === id && els.logContent) {
els.logContent.innerHTML = obj.logEl.innerHTML; els.logContent.innerHTML = obj.logEl.innerHTML;
// Очищаем дублированные строки в Single View после обновления // Очищаем дублированные строки в Single View после обновления
cleanDuplicateLines(els.logContent);
cleanSingleViewEmptyLines(els.logContent); cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
} }
// Очищаем дублированные строки в legacy панели
cleanDuplicateLines(obj.logEl);
cleanSingleViewEmptyLines(obj.logEl);
}); });
// Обновляем мультипросмотр // Обновляем мультипросмотр
@ -1473,7 +1473,8 @@ function refreshAllLogs() {
if (multiViewLog) { if (multiViewLog) {
multiViewLog.innerHTML = filteredHtml.join(''); multiViewLog.innerHTML = filteredHtml.join('');
// Очищаем дублированные строки после обновления // Сразу очищаем пустые строки в мультипросмотре
cleanMultiViewEmptyLines(multiViewLog);
cleanMultiViewDuplicateLines(multiViewLog); cleanMultiViewDuplicateLines(multiViewLog);
} }
}); });
@ -2566,25 +2567,26 @@ function processMultiViewLineBreaks(text) {
} }
/** /**
* Функция для очистки пустых строк в multi view * Функция для радикальной очистки пустых строк в multi view
* Удаляет пустые строки между "Connected" и началом логов * Удаляет все пустые строки и лишние переносы строк
* @param {HTMLElement} multiViewLog - элемент лога multi view * @param {HTMLElement} multiViewLog - элемент лога multi view
*/ */
function cleanMultiViewEmptyLines(multiViewLog) { function cleanMultiViewEmptyLines(multiViewLog) {
if (!multiViewLog) return; if (!multiViewLog) return;
// Находим все строки в логе let removedCount = 0;
const lines = Array.from(multiViewLog.querySelectorAll('.line'));
// Удаляем пустые строки (строки без текста или только с пробелами) // Удаляем все пустые строки (элементы .line без текста)
const lines = Array.from(multiViewLog.querySelectorAll('.line'));
lines.forEach(line => { lines.forEach(line => {
const textContent = line.textContent || line.innerText || ''; const textContent = line.textContent || line.innerText || '';
if (textContent.trim() === '') { if (textContent.trim() === '') {
line.remove(); line.remove();
removedCount++;
} }
}); });
// Также удаляем пустые текстовые узлы // Удаляем все текстовые узлы, которые содержат только пробелы и переносы строк
const walker = document.createTreeWalker( const walker = document.createTreeWalker(
multiViewLog, multiViewLog,
NodeFilter.SHOW_TEXT, NodeFilter.SHOW_TEXT,
@ -2595,12 +2597,27 @@ function cleanMultiViewEmptyLines(multiViewLog) {
const textNodesToRemove = []; const textNodesToRemove = [];
let node; let node;
while (node = walker.nextNode()) { while (node = walker.nextNode()) {
if (node.textContent.trim() === '') { const content = node.textContent;
// Удаляем все узлы, которые содержат только пробелы, переносы строк или табуляцию
if (content.trim() === '') {
textNodesToRemove.push(node); textNodesToRemove.push(node);
} }
} }
textNodesToRemove.forEach(node => node.remove()); textNodesToRemove.forEach(node => node.remove());
// Удаляем все пустые текстовые узлы между элементами .line
const allNodes = Array.from(multiViewLog.childNodes);
for (let i = allNodes.length - 1; i >= 0; i--) {
const node = allNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') {
node.remove();
}
}
if (removedCount > 0) {
console.log(`cleanMultiViewEmptyLines: Удалено ${removedCount} пустых строк`);
}
} }
/** /**
@ -2623,7 +2640,8 @@ function cleanMultiViewDuplicateLines(multiViewLog) {
const currentText = currentLine.textContent || currentLine.innerText || ''; const currentText = currentLine.textContent || currentLine.innerText || '';
const previousText = previousLine.textContent || previousLine.innerText || ''; const previousText = previousLine.textContent || previousLine.innerText || '';
if (currentText.trim() === previousText.trim() && currentText.trim() !== '') { // Удаляем дублированные строки (включая пустые)
if (currentText.trim() === previousText.trim()) {
console.log(`cleanMultiViewDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); console.log(`cleanMultiViewDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`);
currentLine.remove(); currentLine.remove();
removedCount++; removedCount++;
@ -2631,6 +2649,9 @@ function cleanMultiViewDuplicateLines(multiViewLog) {
} }
} }
// После удаления дубликатов очищаем лишние пустые строки
cleanMultiViewEmptyLines(multiViewLog);
if (removedCount > 0) { if (removedCount > 0) {
console.log(`cleanMultiViewDuplicateLines: Удалено ${removedCount} дублированных строк`); console.log(`cleanMultiViewDuplicateLines: Удалено ${removedCount} дублированных строк`);
} }
@ -2656,7 +2677,8 @@ function cleanDuplicateLines(logElement) {
const currentText = currentLine.textContent || currentLine.innerText || ''; const currentText = currentLine.textContent || currentLine.innerText || '';
const previousText = previousLine.textContent || previousLine.innerText || ''; const previousText = previousLine.textContent || previousLine.innerText || '';
if (currentText.trim() === previousText.trim() && currentText.trim() !== '') { // Удаляем дублированные строки (включая пустые)
if (currentText.trim() === previousText.trim()) {
console.log(`cleanDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); console.log(`cleanDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`);
currentLine.remove(); currentLine.remove();
removedCount++; removedCount++;
@ -2664,33 +2686,39 @@ function cleanDuplicateLines(logElement) {
} }
} }
// После удаления дубликатов очищаем лишние пустые строки
if (logElement.classList.contains('multi-view-log')) {
cleanMultiViewEmptyLines(logElement);
} else {
cleanSingleViewEmptyLines(logElement);
}
if (removedCount > 0) { if (removedCount > 0) {
console.log(`cleanDuplicateLines: Удалено ${removedCount} дублированных строк`); console.log(`cleanDuplicateLines: Удалено ${removedCount} дублированных строк`);
} }
} }
/** /**
* Функция для очистки лишних пустых строк в Single View * Функция для радикальной очистки пустых строк в Single View
* Удаляет только лишние пустые строки, сохраняя переносы строк между логами * Удаляет все пустые строки и лишние переносы строк
* @param {HTMLElement} logElement - элемент лога Single View * @param {HTMLElement} logElement - элемент лога Single View
*/ */
function cleanSingleViewEmptyLines(logElement) { function cleanSingleViewEmptyLines(logElement) {
if (!logElement) return; if (!logElement) return;
// Находим все строки в логе
const lines = Array.from(logElement.querySelectorAll('.line'));
let removedCount = 0; let removedCount = 0;
// Удаляем только полностью пустые строки (строки без текста) // Удаляем все пустые строки (элементы .line без текста)
const lines = Array.from(logElement.querySelectorAll('.line'));
lines.forEach(line => { lines.forEach(line => {
const textContent = line.textContent || line.innerText || ''; const textContent = line.textContent || line.innerText || '';
if (textContent.trim() === '' && textContent.length === 0) { if (textContent.trim() === '') {
line.remove(); line.remove();
removedCount++; removedCount++;
} }
}); });
// Удаляем только полностью пустые текстовые узлы (не переносы строк) // Удаляем все текстовые узлы, которые содержат только пробелы и переносы строк
const walker = document.createTreeWalker( const walker = document.createTreeWalker(
logElement, logElement,
NodeFilter.SHOW_TEXT, NodeFilter.SHOW_TEXT,
@ -2701,23 +2729,24 @@ function cleanSingleViewEmptyLines(logElement) {
const textNodesToRemove = []; const textNodesToRemove = [];
let node; let node;
while (node = walker.nextNode()) { while (node = walker.nextNode()) {
// Удаляем только узлы, которые содержат только пробелы и переносы строк const content = node.textContent;
if (node.textContent.trim() === '' && node.textContent.length > 0) { // Удаляем все узлы, которые содержат только пробелы, переносы строк или табуляцию
// Проверяем, что это не просто перенос строки if (content.trim() === '') {
const content = node.textContent; textNodesToRemove.push(node);
if (content === '\n' || content === '\r\n') {
// Оставляем переносы строк
continue;
}
// Удаляем только если это множественные пробелы или табуляция
if (content.match(/^[\s\t]+$/)) {
textNodesToRemove.push(node);
}
} }
} }
textNodesToRemove.forEach(node => node.remove()); textNodesToRemove.forEach(node => node.remove());
// Удаляем все пустые текстовые узлы между элементами .line
const allNodes = Array.from(logElement.childNodes);
for (let i = allNodes.length - 1; i >= 0; i--) {
const node = allNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') {
node.remove();
}
}
if (removedCount > 0) { if (removedCount > 0) {
console.log(`cleanSingleViewEmptyLines: Удалено ${removedCount} пустых строк`); console.log(`cleanSingleViewEmptyLines: Удалено ${removedCount} пустых строк`);
} }
@ -2737,6 +2766,40 @@ function normalizeSpaces(text) {
return text.replace(/\s{2,}/g, ' '); return text.replace(/\s{2,}/g, ' ');
} }
/**
* Функция для периодической очистки пустых строк
* Вызывается автоматически каждые 2 секунды для поддержания чистоты логов
*/
function periodicCleanup() {
// Очищаем пустые строки в Single View
if (!state.multiViewMode && els.logContent) {
cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
}
// Очищаем пустые строки в мультипросмотре
if (state.multiViewMode) {
state.selectedContainers.forEach(containerId => {
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
if (multiViewLog) {
cleanMultiViewEmptyLines(multiViewLog);
cleanMultiViewDuplicateLines(multiViewLog);
}
});
}
// Очищаем пустые строки в legacy панелях
Object.values(state.open).forEach(obj => {
if (obj.logEl) {
cleanSingleViewEmptyLines(obj.logEl);
cleanDuplicateLines(obj.logEl);
}
});
}
// Запускаем периодическую очистку каждые 2 секунды
setInterval(periodicCleanup, 2000);
/** /**
* Функция для обработки специальных замен в MultiView логах * Функция для обработки специальных замен в MultiView логах
* Выполняет специфичные замены для улучшения читаемости логов * Выполняет специфичные замены для улучшения читаемости логов
@ -3140,14 +3203,14 @@ function handleLine(id, line){
if (cls==='err') obj.counters.err++; if (cls==='err') obj.counters.err++;
} }
// Для Single View добавляем перенос строки после каждой строки лога // Для Single View НЕ добавляем перенос строки после каждой строки лога
const html = `<span class="line ${cls}">${ansiToHtml(normalizedLine)}</span>\n`; const html = `<span class="line ${cls}">${ansiToHtml(normalizedLine)}</span>`;
// Сохраняем все логи в буфере (всегда) // Сохраняем все логи в буфере (всегда)
if (!obj.allLogs) obj.allLogs = []; if (!obj.allLogs) obj.allLogs = [];
// Для Single View сохраняем обработанную строку, для MultiView - оригинальную // Для Single View сохраняем обработанную строку, для MultiView - оригинальную
const processedLine = !state.multiViewMode ? processSingleViewSpecialReplacements(normalizedLine) : normalizedLine; const processedLine = !state.multiViewMode ? processSingleViewSpecialReplacements(normalizedLine) : normalizedLine;
const processedHtml = `<span class="line ${cls}">${ansiToHtml(processedLine)}</span>\n`; const processedHtml = `<span class="line ${cls}">${ansiToHtml(processedLine)}</span>`;
obj.allLogs.push({html: processedHtml, line: processedLine, cls: cls}); obj.allLogs.push({html: processedHtml, line: processedLine, cls: cls});
// Ограничиваем размер буфера // Ограничиваем размер буфера
@ -3168,8 +3231,8 @@ function handleLine(id, line){
return; // Пропускаем дублированную строку return; // Пропускаем дублированную строку
} }
// Создаем HTML с обработанной строкой для Single View (с переносом строки) // Создаем HTML с обработанной строкой для Single View (без переноса строки)
const singleViewHtml = `<span class="line ${cls}">${ansiToHtml(singleViewProcessedLine)}</span>\n`; const singleViewHtml = `<span class="line ${cls}">${ansiToHtml(singleViewProcessedLine)}</span>`;
obj.logEl.insertAdjacentHTML('beforeend', singleViewHtml); obj.logEl.insertAdjacentHTML('beforeend', singleViewHtml);
@ -3226,7 +3289,7 @@ function handleLine(id, line){
return; // Пропускаем дублированную строку return; // Пропускаем дублированную строку
} }
const multiViewHtml = `<span class="line ${cls}">${ansiToHtml(processedLine)}</span>\n`; const multiViewHtml = `<span class="line ${cls}">${ansiToHtml(processedLine)}</span>`;
// Добавляем новую строку // Добавляем новую строку
multiViewLog.insertAdjacentHTML('beforeend', multiViewHtml); multiViewLog.insertAdjacentHTML('beforeend', multiViewHtml);
@ -3376,6 +3439,11 @@ async function switchToSingle(svc){
els.logContent.insertAdjacentHTML('beforeend', logEntry.html); els.logContent.insertAdjacentHTML('beforeend', logEntry.html);
} }
}); });
// Очищаем лишние пустые строки после восстановления логов
cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
if (els.autoscroll && els.autoscroll.checked) { if (els.autoscroll && els.autoscroll.checked) {
els.logContent.scrollTop = els.logContent.scrollHeight; els.logContent.scrollTop = els.logContent.scrollHeight;
} }
@ -3774,6 +3842,12 @@ async function refreshLogsAndCounters() {
if (updatedContainer) { if (updatedContainer) {
// Переключаемся на обновленный контейнер // Переключаемся на обновленный контейнер
await switchToSingle(updatedContainer); await switchToSingle(updatedContainer);
// Очищаем лишние пустые строки после переключения
if (els.logContent) {
cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
}
} }
} else { } else {
console.log('No container selected'); console.log('No container selected');
@ -3791,6 +3865,9 @@ els.clearBtn.onclick = ()=> {
// Очищаем современный интерфейс // Очищаем современный интерфейс
if (els.logContent) { if (els.logContent) {
els.logContent.textContent = ''; els.logContent.textContent = '';
// Очищаем лишние пустые строки после очистки
cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
} }
// Очищаем мультипросмотр // Очищаем мультипросмотр
@ -4480,6 +4557,14 @@ window.addEventListener('keydown', async (e)=>{
console.log('LogBoard+ инициализирован с исправлениями дублирования строк и правильными переносами строк в Single View и MultiView режимах'); console.log('LogBoard+ инициализирован с исправлениями дублирования строк и правильными переносами строк в Single View и MultiView режимах');
console.log('Для тестирования используйте: testDuplicateRemoval(), testSingleViewDuplicateRemoval(), testSingleViewEmptyLinesRemoval() или testSingleViewLineBreaks()'); console.log('Для тестирования используйте: testDuplicateRemoval(), testSingleViewDuplicateRemoval(), testSingleViewEmptyLinesRemoval() или testSingleViewLineBreaks()');
// Запускаем первоначальную очистку пустых строк
setTimeout(() => {
if (!state.multiViewMode && els.logContent) {
cleanSingleViewEmptyLines(els.logContent);
cleanDuplicateLines(els.logContent);
}
}, 1000);
})(); })();
</script> </script>
</body> </body>