diff --git a/templates/index.html b/templates/index.html index 3dfef38..eaa0737 100644 --- a/templates/index.html +++ b/templates/index.html @@ -892,6 +892,115 @@ a{color:var(--link)} color: var(--accent); } +/* Чекбоксы для мультивыбора контейнеров */ +.container-select { + position: absolute; + bottom: 8px; + right: 8px; + z-index: 10; +} + +.container-checkbox { + display: none; +} + +.container-checkbox-label { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 2px solid var(--border); + border-radius: 4px; + background: var(--bg); + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.container-checkbox-label:hover { + border-color: var(--accent); + background: var(--chip); +} + +.container-checkbox:checked + .container-checkbox-label { + background: var(--accent); + border-color: var(--accent); +} + +.container-checkbox:checked + .container-checkbox-label::after { + content: "✓"; + color: white; + font-size: 12px; + font-weight: bold; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.container-item.selected { + border-color: var(--accent); + background: var(--tab-active); +} + +/* Мультипросмотр */ +.multi-view-grid { + display: grid; + gap: 2px; + height: 100%; + padding: 0px; +} + +.multi-view-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 2px; +} + +.multi-view-header { + padding: 12px 16px; + background: var(--chip); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.multi-view-title { + font-size: 13px; + font-weight: 500; + color: var(--fg); + margin: 0; + flex: 1; +} + + + +.multi-view-content { + flex: 1; + overflow: hidden; +} + +.multi-view-log { + height: 100%; + margin: 0; + padding: 12px; + font-size: 11px; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-word; + overflow: auto; + background: var(--bg); + color: var(--fg); + font-family: ui-monospace, Menlo, Consolas, monospace; +} + /* Log Area */ .log-area { flex: 1; @@ -990,6 +1099,31 @@ main{display:none} .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} .line{color:var(--fg)} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .dbg{color:#7dcfff} .ts{color:var(--muted)} +/* Цвета для multi-view логов */ +.multi-view-log .line{color:var(--fg) !important} +.multi-view-log .ok{color:var(--ok) !important} +.multi-view-log .warn{color:var(--warn) !important} +.multi-view-log .err{color:var(--err) !important} +.multi-view-log .dbg{color:#7dcfff !important} +.multi-view-log .ts{color:var(--muted) !important} + +/* Дополнительные стили для multi-view логов */ +.multi-view-log span.line{color:var(--fg) !important} +.multi-view-log span.ok{color:var(--ok) !important} +.multi-view-log span.warn{color:var(--warn) !important} +.multi-view-log span.err{color:var(--err) !important} +.multi-view-log span.dbg{color:#7dcfff !important} +.multi-view-log span.ts{color:var(--muted) !important} + +/* Стили для ANSI цветов в multi-view */ +.multi-view-log .ansi-red{color:#f7768e !important} +.multi-view-log .ansi-green{color:#22c55e !important} +.multi-view-log .ansi-yellow{color:#eab308 !important} +.multi-view-log .ansi-blue{color:#3b82f6 !important} +.multi-view-log .ansi-magenta{color:#a855f7 !important} +.multi-view-log .ansi-cyan{color:#06b6d4 !important} +.multi-view-log .ansi-white{color:var(--fg) !important} +.multi-view-log .ansi-black{color:#79808f !important} footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} .filterlvl{display:flex;gap:6px;align-items:center} /* Instance tag */ @@ -1210,6 +1344,8 @@ const state = { layout: 'tabs', // 'tabs' | 'grid2' | 'grid3' | 'grid4' filter: null, levels: {debug:true, info:true, warn:true, err:true}, + selectedContainers: [], // Массив ID выбранных контейнеров для мультипросмотра + multiViewMode: false, // Режим мультипросмотра }; const els = { @@ -1277,6 +1413,7 @@ function setWsState(s){ // Функция для обновления всех логов при изменении фильтров function refreshAllLogs() { + // Обновляем обычный просмотр Object.keys(state.open).forEach(id => { const obj = state.open[id]; if (!obj || !obj.logEl) return; @@ -1303,29 +1440,87 @@ function refreshAllLogs() { els.logContent.innerHTML = obj.logEl.innerHTML; } }); + + // Обновляем мультипросмотр + if (state.multiViewMode) { + state.selectedContainers.forEach(containerId => { + const obj = state.open[containerId]; + if (!obj || !obj.logEl) return; + + // Получаем все логи из буфера + const allLogs = obj.allLogs || []; + const filteredHtml = []; + + allLogs.forEach(logEntry => { + // Проверяем уровень логирования + if (!allowedByLevel(logEntry.cls)) return; + + // Проверяем фильтр + if (!applyFilter(logEntry.line)) return; + + filteredHtml.push(logEntry.html); + }); + + // Обновляем отображение в мультипросмотре + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); + if (multiViewLog) { + multiViewLog.innerHTML = filteredHtml.join(''); + // Отладочная информация + console.log(`Multi-view refresh: Container ${containerId}, ${filteredHtml.length} lines, HTML preview:`, filteredHtml.slice(0, 2).join('').substring(0, 200)); + // Проверяем, что HTML действительно содержит цветные элементы + const coloredElements = multiViewLog.querySelectorAll('.ok, .warn, .err, .dbg, .ansi-red, .ansi-green, .ansi-yellow, .ansi-blue'); + console.log(`Multi-view: Found ${coloredElements.length} colored elements in container ${containerId}`); + } + }); + } } function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } function classify(line){ const l = line.toLowerCase(); - // Проверяем различные форматы уровней логирования - if (/\bdebug\b|level=debug| \[debug\]/.test(l)) return 'dbg'; - if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err'; - if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn'; - if (/\b(info|started|listening|ready|up)\b|level=info/.test(l)) return 'ok'; + // Отладочная информация для понимания что происходит + if (line.includes('INFO') || line.includes('WARNING') || line.includes('ERROR') || line.includes('DEBUG')) { + console.log(`Classifying line: "${line.substring(0, 100)}..."`); + } - // Дополнительные проверки для других форматов + // Проверяем различные форматы уровней логирования (более специфичные сначала) + + // DEBUG - ищем точное совпадение уровня логирования + if (/\s- DEBUG -|\s\[debug\]|level=debug|\bdebug\b(?=\s|$)/.test(l)) { + console.log(`Classified as DEBUG: "${line.substring(0, 100)}..."`); + return 'dbg'; + } + + // ERROR - ищем точное совпадение уровня логирования + if (/\s- ERROR -|\s\[error\]|level=error/.test(l)) { + console.log(`Classified as ERROR: "${line.substring(0, 100)}..."`); + return 'err'; + } + + // WARNING - ищем точное совпадение уровня логирования + if (/\s- WARNING -|\s\[warn\]|level=warn/.test(l)) { + console.log(`Classified as WARNING: "${line.substring(0, 100)}..."`); + return 'warn'; + } + + // INFO - ищем точное совпадение уровня логирования + if (/\s- INFO -|\s\[info\]|level=info/.test(l)) { + console.log(`Classified as INFO: "${line.substring(0, 100)}..."`); + return 'ok'; + } + + // Дополнительные проверки для других форматов (только если не найдены точные совпадения) if (/\bdebug\b/i.test(l)) return 'dbg'; if (/\berror\b/i.test(l)) return 'err'; if (/\bwarning\b/i.test(l)) return 'warn'; if (/\binfo\b/i.test(l)) return 'ok'; - // Отладочная информация для неклассифицированных строк - if (line.includes('level=')) { - console.log(`Unclassified line with level: "${line.substring(0, 200)}..."`); - console.log(`Lowercase version: "${l.substring(0, 200)}..."`); - } + // Отладочная информация для неклассифицированных строк + if (line.includes('level=')) { + console.log(`Unclassified line with level: "${line.substring(0, 200)}..."`); + console.log(`Lowercase version: "${l.substring(0, 200)}..."`); + } return 'other'; } @@ -1484,11 +1679,15 @@ function buildTabs(){ ${escapeHtml(svc.status)} ${svc.url ? `` : ''} +
+ + +
`; item.onclick = (e) => { - // Не переключаем контейнер, если кликнули на ссылку - if (e.target.closest('.container-link')) { + // Не переключаем контейнер, если кликнули на ссылку или чекбокс + if (e.target.closest('.container-link') || e.target.closest('.container-select')) { e.stopPropagation(); return; } @@ -1735,6 +1934,251 @@ async function removeExcludedContainer(containerName) { } } +// Функции для мультивыбора контейнеров +function toggleContainerSelection(containerId) { + const index = state.selectedContainers.indexOf(containerId); + if (index > -1) { + state.selectedContainers.splice(index, 1); + } else { + state.selectedContainers.push(containerId); + } + + updateContainerSelectionUI(); + updateMultiViewMode(); +} + +function updateContainerSelectionUI() { + // Обновляем чекбоксы + document.querySelectorAll('.container-checkbox').forEach(checkbox => { + const containerId = checkbox.getAttribute('data-container-id'); + const containerItem = checkbox.closest('.container-item'); + + if (state.selectedContainers.includes(containerId)) { + checkbox.checked = true; + containerItem.classList.add('selected'); + } else { + checkbox.checked = false; + containerItem.classList.remove('selected'); + } + }); + + // Обновляем заголовок + updateLogTitle(); +} + +function updateMultiViewMode() { + console.log(`updateMultiViewMode called: selectedContainers.length = ${state.selectedContainers.length}, containers:`, state.selectedContainers); + + if (state.selectedContainers.length > 1) { + state.multiViewMode = true; + state.current = null; // Сбрасываем текущий контейнер + console.log('Setting up multi-view mode'); + setupMultiView(); + } else if (state.selectedContainers.length === 1) { + state.multiViewMode = false; + const selectedService = state.services.find(s => s.id === state.selectedContainers[0]); + if (selectedService) { + switchToSingle(selectedService); + } + } else { + // Когда снимаем все галочки, переключаемся в single view + state.multiViewMode = false; + state.current = null; + clearLogArea(); + + // Очищаем область логов и показываем пустое состояние + const logArea = document.querySelector('.log-area'); + if (logArea) { + const logContent = logArea.querySelector('.log-content'); + if (logContent) { + logContent.innerHTML = '
Выберите контейнер для просмотра логов
'; + } + } + + // Обновляем заголовок + if (els.logTitle) { + els.logTitle.textContent = 'LogBoard+'; + } + } + + console.log(`Multi-view mode updated: multiViewMode = ${state.multiViewMode}`); +} + +function setupMultiView() { + console.log('setupMultiView called'); + const logArea = document.querySelector('.log-area'); + if (!logArea) { + console.log('Log area not found'); + return; + } + + // Очищаем область логов + const logContent = logArea.querySelector('.log-content'); + if (logContent) { + logContent.innerHTML = ''; + } + + // Создаем сетку для мультипросмотра + const gridContainer = document.createElement('div'); + gridContainer.className = 'multi-view-grid'; + gridContainer.id = 'multiViewGrid'; + + // Определяем количество колонок в зависимости от количества контейнеров + let columns = 1; + if (state.selectedContainers.length <= 2) columns = 2; + else if (state.selectedContainers.length <= 4) columns = 2; + else if (state.selectedContainers.length <= 6) columns = 3; + else columns = 4; + + gridContainer.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; + + // Создаем панели для каждого выбранного контейнера + state.selectedContainers.forEach(containerId => { + const service = state.services.find(s => s.id === containerId); + if (!service) return; + + const panel = createMultiViewPanel(service); + gridContainer.appendChild(panel); + }); + + if (logContent) { + logContent.appendChild(gridContainer); + } + + // Применяем настройки wrap lines + applyWrapSettings(); + + // Подключаем WebSocket для каждого контейнера + state.selectedContainers.forEach(containerId => { + const service = state.services.find(s => s.id === containerId); + if (service) { + console.log(`Setting up WebSocket for multi-view container: ${service.name} (${containerId})`); + openMultiViewWs(service); + } else { + console.error(`Service not found for container ID: ${containerId}`); + } + }); +} + +function createMultiViewPanel(service) { + console.log(`Creating multi-view panel for service: ${service.name} (${service.id})`); + const panel = document.createElement('div'); + panel.className = 'multi-view-panel'; + panel.setAttribute('data-container-id', service.id); + + panel.innerHTML = ` +
+

${escapeHtml(service.name)}

+
+
+
Connecting...
+
+ `; + + // Проверяем, что элемент создался правильно + 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}`); + } else { + console.error(`Failed to create multi-view log element for ${service.name}`); + } + + console.log(`Multi-view panel created for ${service.name}`); + return panel; +} + +function openMultiViewWs(service) { + const containerId = service.id; + + // Закрываем существующее соединение + closeWs(containerId); + + // Создаем новое WebSocket соединение + const ws = new WebSocket(wsUrl(containerId, service.service, service.project)); + + ws.onopen = () => { + 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...'; + } + }; + + ws.onmessage = (event) => { + console.log(`Multi-view WebSocket received message for ${service.name}: ${event.data.substring(0, 100)}...`); + + const parts = (event.data||'').split(/\r?\n/); + + for (let i=0;i { + console.log(`Multi-view WebSocket closed for ${service.name}`); + }; + + ws.onerror = (error) => { + console.error(`Multi-view WebSocket error for ${service.name}:`, error); + }; + + // Сохраняем соединение с полным набором полей как в openWs + state.open[containerId] = { + ws: ws, + serviceName: service.service, + logEl: document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`), + wrapEl: document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`), + counters: {dbg:0, info:0, warn:0, err:0}, + pausedBuffer: [], + allLogs: [] // Добавляем буфер для логов + }; +} + +function clearLogArea() { + const logContent = document.querySelector('.log-content'); + if (logContent) { + logContent.innerHTML = '
Выберите контейнер для просмотра логов
'; + } + + const logTitle = document.getElementById('logTitle'); + if (logTitle) { + logTitle.textContent = 'LogBoard+'; + } +} + +function updateLogTitle() { + const logTitle = document.getElementById('logTitle'); + if (!logTitle) return; + + if (state.selectedContainers.length === 0) { + logTitle.textContent = 'LogBoard+'; + } else if (state.selectedContainers.length === 1) { + const service = state.services.find(s => s.id === state.selectedContainers[0]); + logTitle.textContent = `${service.name} (${service.service || service.name})`; + } else { + logTitle.textContent = `Multi-view: ${state.selectedContainers.length} containers`; + } +} + +function applyWrapSettings() { + const wrapEnabled = els.wrapToggle && els.wrapToggle.checked; + const wrapStyle = wrapEnabled ? 'pre-wrap' : 'pre'; + + // Применяем к обычному просмотру + document.querySelectorAll('.log').forEach(el => { + el.style.whiteSpace = wrapStyle; + }); + + // Применяем к мультипросмотру + document.querySelectorAll('.multi-view-log').forEach(el => { + el.style.whiteSpace = wrapStyle; + }); +} + async function fetchServices(){ try { console.log('Fetching services...'); @@ -1857,54 +2301,98 @@ function openWs(svc, panel){ }; // Убираем автоматический refresh - теперь только по кнопке +} - function handleLine(id, line){ - const cls = classify(line); - if (cls==='dbg') counters.dbg++; - if (cls==='ok') counters.info++; - if (cls==='warn') counters.warn++; - if (cls==='err') counters.err++; +// Глобальная функция для обработки логов +function handleLine(id, line){ + const obj = state.open[id]; + if (!obj) { + console.error(`handleLine: Object not found for container ${id}, available containers:`, Object.keys(state.open)); + return; + } + + // Отладочная информация для первых нескольких строк + if (!obj.counters) { + console.error(`handleLine: Counters not initialized for container ${id}`); + obj.counters = {dbg:0, info:0, warn:0, err:0}; + } + + const cls = classify(line); + + // Обновляем счетчики + if (obj.counters) { + if (cls==='dbg') obj.counters.dbg++; + if (cls==='ok') obj.counters.info++; + if (cls==='warn') obj.counters.warn++; + if (cls==='err') obj.counters.err++; // Отладочная информация для первых нескольких строк - if (counters.dbg + counters.info + counters.warn + counters.err < 10) { + if (obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 10) { console.log(`Line: "${line.substring(0, 100)}..." -> Class: ${cls}`); } // Отладочная информация о счетчиках - if (counters.dbg + counters.info + counters.warn + counters.err < 5) { - console.log(`Counters: DEBUG=${counters.dbg}, INFO=${counters.info}, WARN=${counters.warn}, ERROR=${counters.err}`); + if (obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 5) { + console.log(`Counters: DEBUG=${obj.counters.dbg}, INFO=${obj.counters.info}, WARN=${obj.counters.warn}, ERROR=${obj.counters.err}`); } - - const html = `${ansiToHtml(line)}\n`; - const obj = state.open[id]; - if (!obj) return; - - // Сохраняем все логи в буфере (всегда) - if (!obj.allLogs) obj.allLogs = []; - obj.allLogs.push({html: html, line: line, cls: cls}); - - // Ограничиваем размер буфера - if (obj.allLogs.length > 10000) { - obj.allLogs = obj.allLogs.slice(-5000); - } - - // Проверяем фильтры для отображения - if (!allowedByLevel(cls)) return; - if (!applyFilter(line)) return; - - // Добавляем логи в отображение + } + + const html = `${ansiToHtml(line)}\n`; + // Отладочная информация для HTML + if (obj.counters && obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 3) { + console.log(`Generated HTML for class ${cls}:`, html.substring(0, 200)); + } + + // Сохраняем все логи в буфере (всегда) + if (!obj.allLogs) obj.allLogs = []; + obj.allLogs.push({html: html, line: line, 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); - if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; + if (els.autoscroll && els.autoscroll.checked && obj.wrapEl) { + obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; + } // Update modern interface if (state.current && state.current.id === id && els.logContent) { els.logContent.innerHTML = obj.logEl.innerHTML; const logContent = document.querySelector('.log-content'); - if (logContent && els.autoscroll.checked) { + if (logContent && els.autoscroll && els.autoscroll.checked) { logContent.scrollTop = logContent.scrollHeight; } } } + + // Update multi-view interface + if (state.multiViewMode && state.selectedContainers.includes(id)) { + console.log(`Multi-view processing: container ${id}, shouldShow: ${shouldShow}, multiViewMode: ${state.multiViewMode}, selectedContainers:`, state.selectedContainers); + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${id}"]`); + if (multiViewLog) { + if (shouldShow) { + multiViewLog.insertAdjacentHTML('beforeend', html); + if (els.autoscroll && els.autoscroll.checked) { + multiViewLog.scrollTop = multiViewLog.scrollHeight; + } + // Отладочная информация для multi-view + console.log(`Multi-view: Added line with class ${cls} to container ${id}, HTML:`, html.substring(0, 100)); + } else { + // Отладочная информация для отфильтрованных строк + console.log(`Multi-view: Filtered out line with class ${cls} from container ${id}, line: "${line.substring(0, 100)}..."`); + } + } else { + // Отладочная информация если элемент не найден + console.log(`Multi-view: Element not found for container ${id}, available elements:`, document.querySelectorAll('.multi-view-log').length); + } + } } function ensurePanel(svc){ @@ -2142,38 +2630,82 @@ function updateCounterVisibility() { // Функция для обновления логов и счетчиков async function refreshLogsAndCounters() { - if (!state.current) { + if (state.multiViewMode && state.selectedContainers.length > 0) { + // Обновляем мультипросмотр + console.log('Refreshing multi-view for containers:', state.selectedContainers); + + // Обновляем счетчики для всех выбранных контейнеров + for (const containerId of state.selectedContainers) { + await updateCounters(containerId); + } + + // Перезапускаем WebSocket соединения для всех выбранных контейнеров + state.selectedContainers.forEach(containerId => { + closeWs(containerId); + const service = state.services.find(s => s.id === containerId); + if (service) { + openMultiViewWs(service); + } + }); + + // Очищаем логи в мультипросмотре + state.selectedContainers.forEach(containerId => { + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); + if (multiViewLog) { + multiViewLog.textContent = 'Refreshing...'; + } + }); + + } else if (state.current) { + // Обычный режим просмотра + console.log('Refreshing logs and counters for:', state.current.id); + + // Обновляем счетчики + await updateCounters(state.current.id); + + // Перезапускаем WebSocket соединение для получения свежих логов + const currentId = state.current.id; + closeWs(currentId); + + // Находим обновленный контейнер в списке + const updatedContainer = state.services.find(s => s.id === currentId); + if (updatedContainer) { + // Переключаемся на обновленный контейнер + switchToSingle(updatedContainer); + } + } else { console.log('No container selected'); - return; - } - - console.log('Refreshing logs and counters for:', state.current.id); - - // Обновляем счетчики - await updateCounters(state.current.id); - - // Перезапускаем WebSocket соединение для получения свежих логов - const currentId = state.current.id; - closeWs(currentId); - - // Находим обновленный контейнер в списке - const updatedContainer = state.services.find(s => s.id === currentId); - if (updatedContainer) { - // Переключаемся на обновленный контейнер - switchToSingle(updatedContainer); } } // Controls els.clearBtn.onclick = ()=> { + // Очищаем обычный просмотр Object.values(state.open).forEach(o => { if (o.logEl) o.logEl.textContent = ''; if (o.allLogs) o.allLogs = []; // Очищаем буфер логов }); + // Очищаем современный интерфейс if (els.logContent) { els.logContent.textContent = ''; } + + // Очищаем мультипросмотр + if (state.multiViewMode) { + state.selectedContainers.forEach(containerId => { + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); + if (multiViewLog) { + multiViewLog.textContent = ''; + } + // Очищаем буфер логов для мультипросмотра + const obj = state.open[containerId]; + if (obj && obj.allLogs) { + obj.allLogs = []; + } + }); + } + // Сбрасываем счетчики document.querySelectorAll('.cdbg, .cinfo, .cwarn, .cerr').forEach(el => { el.textContent = '0'; @@ -2225,6 +2757,10 @@ function addCounterClickHandlers() { if (state.current) { refreshLogsAndCounters(); } + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } @@ -2237,6 +2773,10 @@ function addCounterClickHandlers() { if (state.current) { refreshLogsAndCounters(); } + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } @@ -2249,6 +2789,10 @@ function addCounterClickHandlers() { if (state.current) { refreshLogsAndCounters(); } + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } @@ -2261,6 +2805,10 @@ function addCounterClickHandlers() { if (state.current) { refreshLogsAndCounters(); } + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } } @@ -2482,10 +3030,7 @@ if (els.tail) { } if (els.wrapToggle) { els.wrapToggle.onchange = ()=> { - document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre'); - if (els.logContent) { - els.logContent.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre'; - } + applyWrapSettings(); }; } @@ -2509,6 +3054,16 @@ if (els.autoscroll) { logContent.scrollTop = logContent.scrollHeight; } } + + // Обновляем мультипросмотр + if (state.multiViewMode) { + state.selectedContainers.forEach(containerId => { + const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); + if (multiViewLog && els.autoscroll.checked) { + multiViewLog.scrollTop = multiViewLog.scrollHeight; + } + }); + } }; } @@ -2526,6 +3081,10 @@ if (els.lvlDebug) { state.levels.debug = els.lvlDebug.checked; updateCounterVisibility(); refreshAllLogs(); + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } if (els.lvlInfo) { @@ -2533,6 +3092,10 @@ if (els.lvlInfo) { state.levels.info = els.lvlInfo.checked; updateCounterVisibility(); refreshAllLogs(); + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } if (els.lvlWarn) { @@ -2540,6 +3103,10 @@ if (els.lvlWarn) { state.levels.warn = els.lvlWarn.checked; updateCounterVisibility(); refreshAllLogs(); + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } if (els.lvlErr) { @@ -2547,6 +3114,10 @@ if (els.lvlErr) { state.levels.err = els.lvlErr.checked; updateCounterVisibility(); refreshAllLogs(); + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } }; } @@ -2621,6 +3192,14 @@ window.addEventListener('keydown', (e)=>{ } }; } + + // Добавляем обработчики для чекбоксов контейнеров + document.addEventListener('change', (e) => { + if (e.target.classList.contains('container-checkbox')) { + const containerId = e.target.getAttribute('data-container-id'); + toggleContainerSelection(containerId); + } + }); })();