/** * LogBoard+ - Веб-панель для просмотра логов микросервисов * Автор: Сергей Антропов * Сайт: https://devops.org.ru * Версия: 2.0 */ console.log('LogBoard+ script loaded - VERSION 2'); /** * Глобальное состояние приложения * Содержит все данные о контейнерах, настройках и режимах отображения */ const state = { services: [], // Список всех доступных сервисов current: null, // Текущий выбранный контейнер для single view open: {}, // Открытые WebSocket соединения: id -> {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName} layout: 'tabs', // Режим отображения: 'tabs' | 'grid2' | 'grid3' | 'grid4' filter: null, // Текущий фильтр для логов levels: {debug:true, info:true, warn:true, err:true, other:true}, // Уровни логирования для отображения selectedContainers: [], // Массив ID выбранных контейнеров для мультипросмотра multiViewMode: false, // Режим мультипросмотра (true = multi-view, false = single-view) }; /** * Ссылки на DOM элементы интерфейса * Содержит все элементы управления и отображения */ const els = { // Legacy elements (старые элементы для обратной совместимости) tabs: document.getElementById('tabs'), // Контейнер с вкладками grid: document.getElementById('grid'), // Контейнер с сеткой tail: document.getElementById('tail'), // Поле ввода количества строк логов autoscroll: document.getElementById('autoscroll'), // Чекбокс автопрокрутки wrapToggle: document.getElementById('wrap'), // Переключатель переноса строк filter: document.getElementById('filter'), // Поле фильтра логов wsstate: document.getElementById('wsstate'), // Индикатор состояния WebSocket ajaxUpdateBtn: document.getElementById('ajaxUpdateBtn'), // Кнопка AJAX обновления projectBadge: document.getElementById('projectBadge'), // Бейдж текущего проекта clearBtn: document.getElementById('clear'), // Кнопка очистки логов refreshBtn: document.getElementById('refresh'), // Кнопка обновления snapshotBtn: document.getElementById('snapshot'), // Кнопка создания снимка lvlDebug: document.getElementById('lvlDebug'), // Кнопка уровня DEBUG lvlInfo: document.getElementById('lvlInfo'), // Кнопка уровня INFO lvlWarn: document.getElementById('lvlWarn'), // Кнопка уровня WARN lvlErr: document.getElementById('lvlErr'), // Кнопка уровня ERROR lvlOther: document.getElementById('lvlOther'), // Кнопка уровня OTHER layoutBadge: document.getElementById('layoutBadge') || { textContent: '' }, // Бейдж режима отображения aggregate: document.getElementById('aggregate') || { checked: false }, // Чекбокс агрегации themeSwitch: document.getElementById('themeSwitch'), // Переключатель темы copyFab: document.getElementById('copyFab'), // Кнопка копирования groupBtn: document.getElementById('groupBtn') || { onclick: null }, // Кнопка группировки // New modern elements (новые элементы современного интерфейса) containerList: document.getElementById('containerList'), // Список контейнеров logContent: document.getElementById('logContent'), // Основной контент логов mobileToggle: document.getElementById('mobileToggle'), // Переключатель мобильного режима optionsBtn: document.getElementById('optionsBtn'), // Кнопка настроек helpBtn: document.getElementById('helpBtn'), // Кнопка помощи logoutBtn: document.getElementById('logoutBtn'), // Кнопка выхода sidebar: document.getElementById('sidebar'), // Боковая панель sidebarToggle: document.getElementById('sidebarToggle'), // Переключатель боковой панели header: document.getElementById('header'), // Заголовок hotkeysModal: document.getElementById('hotkeysModal'), // Модальное окно горячих клавиш hotkeysModalClose: document.getElementById('hotkeysModalClose'), // Кнопка закрытия модального окна multiViewPanel: document.getElementById('multiViewPanel'), // Панель мультипросмотра multiViewPanelTitle: document.getElementById('multiViewPanelTitle'), // Заголовок мультипросмотра singleViewPanel: document.getElementById('singleViewPanel'), // Панель одиночного просмотра singleViewTitle: document.getElementById('singleViewTitle'), // Заголовок одиночного просмотра }; /** * Инициализация переключателя темы * Загружает сохраненную тему из localStorage и настраивает переключатель */ (function initTheme(){ const saved = localStorage.lb_theme || 'dark'; document.documentElement.setAttribute('data-theme', saved); els.themeSwitch.checked = (saved==='light'); els.themeSwitch.addEventListener('change', ()=>{ const t = els.themeSwitch.checked ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', t); localStorage.lb_theme = t; }); })(); /** * Устанавливает состояние WebSocket соединения в интерфейсе * @param {string} s - Состояние: 'on', 'off', 'err', 'available' */ function setWsState(s){ console.log('setWsState: Устанавливаем состояние', s); console.log('setWsState: Текущие соединения:', Object.keys(state.open)); els.wsstate.textContent = 'ws: ' + s; // Удаляем все классы состояний els.wsstate.classList.remove('ws-on', 'ws-off', 'ws-err', 'ws-available'); // Добавляем соответствующий класс if (s === 'on') { els.wsstate.classList.add('ws-on'); } else if (s === 'off') { els.wsstate.classList.add('ws-off'); } else if (s === 'err') { els.wsstate.classList.add('ws-err'); } else if (s === 'available') { els.wsstate.classList.add('ws-available'); } } /** * Определяет общее состояние WebSocket соединений * Проверяет все открытые соединения и устанавливает соответствующее состояние */ function determineWsState() { const openConnections = Object.keys(state.open); console.log('determineWsState: Проверяем', openConnections.length, 'соединений'); console.log('determineWsState: Все соединения:', openConnections); // Если нет открытых соединений, проверяем сервер через AJAX if (openConnections.length === 0) { console.log('determineWsState: Нет соединений, проверяем сервер'); // Асинхронно проверяем сервер, но возвращаем 'off' для немедленного отображения // Если сервер доступен, checkWebSocketStatus установит 'on' setTimeout(() => { checkWebSocketStatus(); }, 100); return 'off'; } // Проверяем состояние всех соединений let hasActiveConnection = false; let hasConnecting = false; let closedConnections = []; let errorConnections = []; for (const id of openConnections) { const obj = state.open[id]; if (obj && obj.ws) { console.log(`determineWsState: Соединение ${id}, readyState:`, obj.ws.readyState, 'WebSocket:', obj.ws); if (obj.ws.readyState === WebSocket.OPEN) { hasActiveConnection = true; console.log(`determineWsState: Соединение ${id} активно`); } else if (obj.ws.readyState === WebSocket.CONNECTING) { hasConnecting = true; console.log(`determineWsState: Соединение ${id} подключается`); } else if (obj.ws.readyState === WebSocket.CLOSED || obj.ws.readyState === WebSocket.CLOSING) { closedConnections.push(id); console.log(`determineWsState: Соединение ${id} закрыто/закрывается`); } } else { console.log(`determineWsState: Соединение ${id} не найдено или нет WebSocket, obj:`, obj); closedConnections.push(id); } } // Удаляем закрытые соединения closedConnections.forEach(id => { console.log(`determineWsState: Удаляем закрытое соединение ${id}`); delete state.open[id]; }); // Если есть активные соединения или есть соединения в процессе установки if (hasActiveConnection || hasConnecting) { console.log('determineWsState: Есть активные/подключающиеся соединения, возвращаем on'); return 'on'; } else { console.log('determineWsState: Нет активных соединений, проверяем сервер'); // Асинхронно проверяем сервер, но возвращаем 'off' для немедленного отображения // Если сервер доступен, checkWebSocketStatus установит 'on' setTimeout(() => { checkWebSocketStatus(); }, 100); return 'off'; } } // Функция для проверки состояния WebSocket через AJAX async function checkWebSocketStatus() { try { const token = localStorage.getItem('access_token'); if (!token) { console.log('checkWebSocketStatus: Нет токена, устанавливаем off'); setWsState('off'); return; } console.log('checkWebSocketStatus: Отправляем запрос к /api/websocket/status'); const response = await fetch('/api/websocket/status', { headers: { 'Authorization': `Bearer ${token}` } }); console.log('checkWebSocketStatus: Получен ответ, статус:', response.status, response.statusText); if (response.ok) { const data = await response.json(); console.log('checkWebSocketStatus: Получен ответ от сервера:', data); if (data.status === 'available') { // Проверяем активные клиентские соединения const openConnections = Object.keys(state.open); let hasActiveConnection = false; for (const id of openConnections) { const obj = state.open[id]; if (obj && obj.ws && obj.ws.readyState === WebSocket.OPEN) { hasActiveConnection = true; break; } } // Если сервер доступен, всегда показываем 'on' console.log('checkWebSocketStatus: Сервер доступен, устанавливаем on'); setWsState('on'); } else if (data.status === 'no_containers') { console.log('checkWebSocketStatus: Нет контейнеров, устанавливаем off'); setWsState('off'); } else { console.log('checkWebSocketStatus: Ошибка сервера, устанавливаем err'); setWsState('err'); } } else { console.log('checkWebSocketStatus: HTTP ошибка, устанавливаем err'); setWsState('err'); } } catch (error) { console.error('checkWebSocketStatus: Ошибка запроса:', error); setWsState('err'); } } // Интервал для автоматической проверки состояния WebSocket let wsStatusInterval = null; // Функция для запуска автоматической проверки состояния WebSocket function startWebSocketStatusCheck() { if (wsStatusInterval) { clearInterval(wsStatusInterval); } // Проверяем каждые 3 секунды wsStatusInterval = setInterval(() => { console.log('Автоматическая проверка состояния WebSocket'); checkWebSocketStatus(); }, 3000); console.log('Запущена автоматическая проверка состояния WebSocket'); } // Функция для остановки автоматической проверки function stopWebSocketStatusCheck() { if (wsStatusInterval) { clearInterval(wsStatusInterval); wsStatusInterval = null; console.log('Остановлена автоматическая проверка состояния WebSocket'); } } /** * Устанавливает визуальное состояние кнопки AJAX обновления * @param {boolean} enabled - Включено ли AJAX обновление */ function setAjaxUpdateState(enabled) { console.log('setAjaxUpdateState: enabled =', enabled, 'els.ajaxUpdateBtn =', !!els.ajaxUpdateBtn); if (els.ajaxUpdateBtn) { // Удаляем все классы состояний els.ajaxUpdateBtn.classList.remove('ajax-on', 'ajax-off'); // Добавляем соответствующий класс if (enabled) { els.ajaxUpdateBtn.classList.add('ajax-on'); els.ajaxUpdateBtn.textContent = 'update'; console.log('setAjaxUpdateState: Устанавливаем зеленый цвет (ajax-on)'); } else { els.ajaxUpdateBtn.classList.add('ajax-off'); els.ajaxUpdateBtn.textContent = 'update'; console.log('setAjaxUpdateState: Устанавливаем красный цвет (ajax-off)'); } } else { console.error('setAjaxUpdateState: Кнопка ajaxUpdateBtn не найдена!'); } } /** * Обновляет отображение всех логов при изменении фильтров * Перерисовывает логи с учетом текущих настроек фильтрации и уровней */ function refreshAllLogs() { // Обновляем обычный просмотр Object.keys(state.open).forEach(id => { const obj = state.open[id]; 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); }); // Обновляем отображение obj.logEl.innerHTML = filteredHtml.join(''); // Сразу очищаем пустые строки в legacy панели cleanSingleViewEmptyLines(obj.logEl); cleanDuplicateLines(obj.logEl); // Обновляем современный интерфейс if (state.current && state.current.id === id && els.logContent) { els.logContent.innerHTML = obj.logEl.innerHTML; // Очищаем дублированные строки в Single View после обновления cleanSingleViewEmptyLines(els.logContent); cleanDuplicateLines(els.logContent); } }); // Обновляем мультипросмотр 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(''); // Сразу очищаем пустые строки в мультипросмотре cleanMultiViewEmptyLines(multiViewLog); cleanMultiViewDuplicateLines(multiViewLog); } }); } // Пересчитываем счетчики в зависимости от режима после обновления логов setTimeout(() => { if (state.multiViewMode) { recalculateMultiViewCounters(); } else { recalculateCounters(); } }, 100); } /** * Экранирует HTML символы для безопасного отображения * @param {string} s - Строка для экранирования * @returns {string} Экранированная строка */ function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } /** * Классифицирует строку лога по уровню логирования * Определяет уровень на основе ключевых слов и паттернов в строке * @param {string} line - Строка лога для классификации * @returns {string} Класс уровня: 'dbg', 'err', 'warn', 'ok', 'other' */ function classify(line){ const l = line.toLowerCase(); // Проверяем различные форматы уровней логирования (более специфичные сначала) // DEBUG - ищем точное совпадение уровня логирования if (/\s- DEBUG -|\s\[debug\]|level=debug|\bdebug\b(?=\s|$)/.test(l)) { return 'dbg'; } // ERROR - ищем точное совпадение уровня логирования if (/\s- ERROR -|\s\[error\]|level=error/.test(l)) { return 'err'; } // FATAL - ищем точное совпадение уровня логирования (раскрашиваем как ERROR) if (/\s- FATAL -|\s\[fatal\]|level=fatal/.test(l)) { return 'err'; } // PostgreSQL FATAL - специальная проверка для FATAL логов PostgreSQL if (/\[\d+\]\s+FATAL:/i.test(l)) { return 'err'; } // Простая проверка для любых строк содержащих "FATAL:" if (l.includes('fatal:')) { return 'err'; } // WARNING - ищем точное совпадение уровня логирования if (/\s- WARNING -|\s\[warn\]|level=warn/.test(l)) { return 'warn'; } // INFO - ищем точное совпадение уровня логирования if (/\s- INFO -|\s\[info\]|level=info/.test(l)) { return 'ok'; } // LOG - ищем точное совпадение уровня логирования (раскрашиваем как INFO) if (/\s- LOG -|\s\[log\]|level=log|^log:/i.test(l)) { return 'ok'; } // PostgreSQL LOG - специальная проверка для логов PostgreSQL if (/\[\d+\]\s+LOG:/i.test(l)) { return 'ok'; } // Простая проверка для любых строк содержащих "LOG:" if (l.includes('log:')) { return 'ok'; } // Дополнительные проверки для других форматов (только если не найдены точные совпадения) if (/\bdebug\b/i.test(l)) return 'dbg'; if (/\berror\b/i.test(l)) return 'err'; if (/\bfatal\b/i.test(l)) { return 'err'; // FATAL также раскрашиваем как ERROR } if (/\bwarning\b/i.test(l)) return 'warn'; if (/\binfo\b/i.test(l)) return 'ok'; if (/\blog\b/i.test(l)) { return 'ok'; // LOG также раскрашиваем как INFO } // Отладка для неклассифицированных логов if (l.includes('log:') || l.includes('fatal:')) { console.log('Unclassified LOG/FATAL line:', line); } return 'other'; } /** * Проверяет, разрешен ли отображение лога данного уровня * @param {string} cls - Класс уровня лога ('dbg', 'err', 'warn', 'ok', 'other') * @returns {boolean} Разрешен ли отображение */ function allowedByLevel(cls){ if (cls==='dbg') return state.levels.debug; if (cls==='err') return state.levels.err; if (cls==='warn') return state.levels.warn; if (cls==='ok') return state.levels.info; if (cls==='other') return state.levels.other; // Показываем неклассифицированные строки в зависимости от настройки return true; } /** * Проверяет, разрешен ли отображение лога данного уровня для конкретного контейнера * Используется в режиме мультипросмотра для индивидуальных настроек контейнеров * @param {string} cls - Класс уровня лога ('dbg', 'err', 'warn', 'ok', 'other') * @param {string} containerId - ID контейнера * @returns {boolean} Разрешен ли отображение */ function allowedByContainerLevel(cls, containerId) { // Если настройки контейнера не инициализированы, инициализируем их if (!state.containerLevels) { state.containerLevels = {}; } if (!state.containerLevels[containerId]) { state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true}; } const containerLevels = state.containerLevels[containerId]; let result; if (cls==='dbg') result = containerLevels.debug; else if (cls==='err') result = containerLevels.err; else if (cls==='warn') result = containerLevels.warn; else if (cls==='ok') result = containerLevels.info; else if (cls==='other') result = containerLevels.other; else result = true; console.log(`allowedByContainerLevel: containerId=${containerId}, cls=${cls}, result=${result}, levels=`, containerLevels); return result; } /** * Обновляет видимость логов в Single View режиме * Перерисовывает логи с учетом текущих фильтров и настроек уровней * @param {HTMLElement} logElement - Элемент для обновления */ function updateLogVisibility(logElement) { if (!logElement || !state.current) return; const containerId = state.current.id; const obj = state.open[containerId]; if (!obj || !obj.allLogs) return; // Пересоздаем содержимое лога с учетом фильтров, сохраняя HTML-разметку let visibleHtml = ''; obj.allLogs.forEach(logEntry => { const shouldShow = allowedByLevel(logEntry.cls) && applyFilter(logEntry.line); if (shouldShow) { visibleHtml += logEntry.html + '\n'; } }); logElement.innerHTML = visibleHtml; // Обновляем счетчики recalculateCounters(); // Обновляем состояние кнопок уровней логирования только для single-view const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn'); singleLevelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const isActive = state.levels[level]; btn.classList.toggle('active', isActive); btn.classList.toggle('disabled', !isActive); }); } // Функция для обновления видимости логов конкретного контейнера в Multi View function updateContainerLogVisibility(containerId) { if (!state.multiViewMode) return; console.log(`updateContainerLogVisibility: Обновляем видимость логов для контейнера ${containerId}`); const logElement = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (!logElement) return; const obj = state.open[containerId]; if (!obj || !obj.allLogs) return; // Пересоздаем содержимое лога с учетом фильтров контейнера, сохраняя HTML-разметку let visibleHtml = ''; obj.allLogs.forEach(logEntry => { const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && applyFilter(logEntry.line); if (shouldShow) { visibleHtml += logEntry.html + '\n'; } }); logElement.innerHTML = visibleHtml; // Обновляем счетчики для этого контейнера updateContainerCounters(containerId); // Обновляем состояние кнопок уровней логирования только для этого контейнера const levelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`); levelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); // Используем настройки контейнера, если они есть const containerLevels = state.containerLevels && state.containerLevels[containerId] ? state.containerLevels[containerId] : {debug: true, info: true, warn: true, err: true, other: true}; const isActive = containerLevels[level]; btn.classList.toggle('active', isActive); btn.classList.toggle('disabled', !isActive); }); } // Функция для обновления счетчиков конкретного контейнера function updateContainerCounters(containerId) { const obj = state.open[containerId]; if (!obj || !obj.allLogs) return; // Получаем значение Tail Lines const tailLines = parseInt(els.tail.value) || 50; // Берем только последние N логов const visibleLogs = obj.allLogs.slice(-tailLines); // Сбрасываем счетчики obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0}; // Пересчитываем счетчики только для отображаемых логов visibleLogs.forEach(logEntry => { const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && 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++; if (logEntry.cls === 'other') obj.counters.other++; } }); // Обновляем отображение счетчиков в кнопках заголовка const levelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`); levelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const valueEl = btn.querySelector('.level-value'); if (valueEl) { switch (level) { case 'debug': valueEl.textContent = obj.counters.dbg; break; case 'info': valueEl.textContent = obj.counters.info; break; case 'warn': valueEl.textContent = obj.counters.warn; break; case 'err': valueEl.textContent = obj.counters.err; break; case 'other': valueEl.textContent = obj.counters.other; break; } } }); } // Функция для обновления счетчиков в кнопках заголовков function updateHeaderCounters(containerId, counters) { // Обновляем счетчики для single-view (если это текущий контейнер) if (state.current && state.current.id === containerId) { const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn'); singleLevelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const valueEl = btn.querySelector('.level-value'); if (valueEl) { switch (level) { case 'debug': valueEl.textContent = counters.dbg; break; case 'info': valueEl.textContent = counters.info; break; case 'warn': valueEl.textContent = counters.warn; break; case 'err': valueEl.textContent = counters.err; break; case 'other': valueEl.textContent = counters.other; break; } } }); } // Обновляем счетчики для multi-view (только для конкретного контейнера) if (state.multiViewMode && state.selectedContainers.includes(containerId)) { const multiLevelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`); multiLevelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const valueEl = btn.querySelector('.level-value'); if (valueEl) { switch (level) { case 'debug': valueEl.textContent = counters.dbg; break; case 'info': valueEl.textContent = counters.info; break; case 'warn': valueEl.textContent = counters.warn; break; case 'err': valueEl.textContent = counters.err; break; case 'other': valueEl.textContent = counters.other; break; } } }); } } // Функция для инициализации состояния кнопок уровней логирования function initializeLevelButtons() { // Восстанавливаем состояние кнопок loglevels из localStorage const savedLevelsState = getLogLevelsStateFromStorage(); if (savedLevelsState) { console.log('Restoring log levels state from localStorage'); // Восстанавливаем глобальные настройки для single-view if (savedLevelsState.globalLevels) { state.levels = { ...state.levels, ...savedLevelsState.globalLevels }; } // Восстанавливаем настройки контейнеров для multi-view if (savedLevelsState.containerLevels) { state.containerLevels = { ...state.containerLevels, ...savedLevelsState.containerLevels }; } } // Инициализируем кнопки для single-view const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn'); singleLevelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const isActive = state.levels[level]; btn.classList.toggle('active', isActive); btn.classList.toggle('disabled', !isActive); }); // Инициализируем кнопки для multi-view (если есть) const multiLevelBtns = document.querySelectorAll('.multi-view-levels .level-btn'); multiLevelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const containerId = btn.getAttribute('data-container-id'); // Инициализируем настройки контейнера, если их нет if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) { if (!state.containerLevels) { state.containerLevels = {}; } state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true}; } // Используем настройки контейнера const isActive = state.containerLevels && state.containerLevels[containerId] ? state.containerLevels[containerId][level] : true; btn.classList.toggle('active', isActive); btn.classList.toggle('disabled', !isActive); }); // Обновляем стили логов после инициализации кнопок updateLogStyles(); // Применяем настройки wrap text applyWrapSettings(); } /** * Применяет фильтр к строке лога * Проверяет, соответствует ли строка текущему фильтру (безопасный regex поиск) * @param {string} line - Строка лога для проверки * @returns {boolean} Проходит ли строка фильтр */ function applyFilter(line){ if(!state.filter) return true; try{ // Экранируем специальные символы regex для безопасного поиска const escapedFilter = state.filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(escapedFilter, 'i').test(line); }catch(e){ console.error('Filter error:', e); return true; } } /** * Константы и настройки для работы с ANSI цветами * SGR (Select Graphic Rendition): 0/1/3/4, 30-37 */ /** * Настройки экземпляров контейнеров * Содержит цвета, фильтры и палитру для визуального различия контейнеров */ const inst = { colors: {}, // Кэш цветов для контейнеров filters: {}, // Фильтры для экземпляров palette: [ // Палитра цветов для контейнеров '#7aa2f7','#9ece6a','#e0af68','#f7768e','#bb9af7','#7dcfff','#c0caf5','#f6bd60', '#84cc16','#06b6d4','#fb923c','#ef4444','#22c55e','#a855f7' ] }; /** * Генерирует уникальный цвет для контейнера на основе его ID * Использует хеш-функцию для детерминированного выбора цвета из палитры * @param {string} id8 - Первые 8 символов ID контейнера * @returns {string} HEX цвет для контейнера */ function idColor(id8){ if (inst.colors[id8]) return inst.colors[id8]; // simple hash to pick from palette let h = 0; for (let i=0;i>>0; } const color = inst.palette[h % inst.palette.length]; inst.colors[id8] = color; return color; } function updateIdFiltersBar(){ const bar = document.getElementById('idFilters'); bar.innerHTML = ''; const ids = Object.keys(inst.filters); if (!ids.length){ bar.style.display='none'; return; } bar.style.display='flex'; ids.forEach(id8=>{ const wrap = document.createElement('label'); wrap.style.display='inline-flex'; wrap.style.alignItems='center'; wrap.style.gap='6px'; const cb = document.createElement('input'); cb.type='checkbox'; cb.checked = inst.filters[id8] !== false; cb.onchange = ()=> inst.filters[id8] = cb.checked; const chip = document.createElement('span'); chip.className='inst-tag'; chip.style.borderColor = idColor(id8); chip.style.color = idColor(id8); chip.textContent = id8; wrap.appendChild(cb); wrap.appendChild(chip); bar.appendChild(wrap); }); } function shouldShowInstance(id8){ if (!Object.keys(inst.filters).length) return true; const val = inst.filters[id8]; return val !== false; } function parsePrefixAndStrip(line){ // Accept "[id]" or "[id service]" prefixes from fan/fan_group const m = line.match(/^\[([0-9a-f]{8})(?:\s+[^\]]+)?\]\s(.*)$/i); if (!m) return null; return {id8: m[1], rest: m[2]}; } /** * Конвертирует ANSI escape-последовательности в HTML * Поддерживает цвета (30-37), жирный (1), курсив (3), подчеркивание (4) * @param {string} text - Текст с ANSI кодами * @returns {string} HTML с CSS классами для стилизации */ function ansiToHtml(text){ const ESC = '\u001b['; const parts = text.split(ESC); if (parts.length === 1) return escapeHtml(text); let html = escapeHtml(parts[0]); let classes = []; for (let i=1;i=30 && c<=37){ classes = classes.filter(x=>!x.startsWith('ansi-')); const map = {30:'black',31:'red',32:'green',33:'yellow',34:'blue',35:'magenta',36:'cyan',37:'white'}; classes.push('ansi-'+map[c]); } } if (classes.length) html += `` + escapeHtml(rest) + ``; else html += escapeHtml(rest); } return html; } function panelTemplate(svc){ const div = document.createElement('div'); div.className='panel'; div.dataset.cid = svc.id; div.innerHTML = `
${escapeHtml((svc.project?`[${svc.project}] `:'')+(svc.service||svc.name))} dbg:0 info:0 warn:0 err:0
`; return div; } function buildTabs(){ // Legacy tabs (hidden) els.tabs.innerHTML=''; state.services.forEach(svc=>{ const b = document.createElement('button'); b.className='tab' + ((state.current && svc.id===state.current.id) ? ' active':''); b.textContent = (svc.project?(`[${svc.project}] `):'') + (svc.service||svc.name); b.title = `${svc.name} • ${svc.image} • ${svc.status}`; b.onclick = async ()=> await switchToSingle(svc); els.tabs.appendChild(b); }); // Modern container list els.containerList.innerHTML = ''; // Миникарточки контейнеров const miniContainerList = document.getElementById('miniContainerList'); if (miniContainerList) { miniContainerList.innerHTML = ''; } state.services.forEach(svc => { // Создаем обычную карточку контейнера const item = document.createElement('div'); item.className = 'container-item'; if (state.current && svc.id === state.current.id) { item.classList.add('active'); } item.setAttribute('data-cid', svc.id); const statusClass = svc.status === 'running' ? 'running' : svc.status === 'stopped' ? 'stopped' : 'paused'; item.innerHTML = `
${escapeHtml(svc.name)}
${escapeHtml(svc.service || svc.name)} • ${escapeHtml(svc.project || 'standalone')}
${escapeHtml(svc.status)} ${svc.status === 'running' && svc.host_port ? ` on ${svc.host_port} port` : ''} ${svc.url ? `` : ''}
`; // Устанавливаем состояние selected для контейнера if (state.selectedContainers.includes(svc.id)) { item.classList.add('selected'); } item.onclick = async (e) => { // Не переключаем контейнер, если кликнули на ссылку или чекбокс if (e.target.closest('.container-link') || e.target.closest('.container-select')) { e.stopPropagation(); return; } await switchToSingle(svc); }; els.containerList.appendChild(item); // Создаем миникарточку контейнера if (miniContainerList) { const miniItem = document.createElement('div'); miniItem.className = 'mini-container-item'; if (state.current && svc.id === state.current.id) { miniItem.classList.add('active'); } miniItem.setAttribute('data-cid', svc.id); // Сокращаем имя для миникарточки const shortName = svc.name.length > 8 ? svc.name.substring(0, 6) + '..' : svc.name; miniItem.innerHTML = `
${escapeHtml(shortName)}
`; // Добавляем обработчики для всплывающих подсказок miniItem.addEventListener('mouseenter', (e) => { showMiniContainerTooltip(e, svc); }); miniItem.addEventListener('mouseleave', () => { // Добавляем задержку перед скрытием подсказки const tooltip = document.getElementById('miniContainerTooltip'); if (tooltip) { tooltip.hideTimer = setTimeout(() => { if (tooltip && !tooltip.matches(':hover')) { hideMiniContainerTooltip(); } }, 150); } }); // Обработчик клика для миникарточек с поддержкой Shift+клик и Ctrl+клик miniItem.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (e.shiftKey && lastSelectedContainerId && lastSelectedContainerId !== svc.id) { // Shift+клик с предыдущим выбором - диапазонный выбор console.log('Shift+клик для диапазонного выбора:', lastSelectedContainerId, 'to', svc.id); selectContainerRange(lastSelectedContainerId, svc.id); } else if (e.shiftKey) { // Shift+клик - добавляем/убираем из мультивыбора console.log('Shift+клик на миникарточку:', svc.name); toggleContainerSelection(svc.id); lastSelectedContainerId = svc.id; } else if (e.ctrlKey || e.metaKey) { // Ctrl/Cmd+клик - добавляем/убираем из мультивыбора console.log('Ctrl+клик на миникарточку:', svc.name); toggleContainerSelection(svc.id); lastSelectedContainerId = svc.id; } else { // Обычный клик - переключаемся в single view console.log('Обычный клик на миникарточку:', svc.name); lastSelectedContainerId = svc.id; await switchToSingle(svc); } }); miniContainerList.appendChild(miniItem); } }); } function setLayout(cls){ state.layout = cls; if (els.layoutBadge) { els.layoutBadge.textContent = 'view: ' + (cls==='tabs'?'tabs':cls); } els.grid.className = cls==='tabs' ? 'grid-1' : (cls==='grid2'?'grid-2':cls==='grid3'?'grid-3':'grid-4'); } async function fetchProjects(){ try { console.log('Fetching projects...'); const url = new URL(location.origin + '/api/containers/projects'); const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); window.location.href = '/login'; return; } const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok){ if (res.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return; } console.error('Failed to fetch projects:', res.status, res.statusText); return; } const projects = await res.json(); console.log('Projects loaded:', projects); // Обновляем мультивыбор проектов в заголовке const dropdown = document.getElementById('projectSelectDropdown'); const display = document.getElementById('projectSelectDisplay'); const displayText = display?.querySelector('.multi-select-text'); console.log('Multi-select elements found:', {dropdown: !!dropdown, display: !!display, displayText: !!displayText}); if (dropdown && displayText) { // Очищаем dropdown dropdown.innerHTML = ''; // Добавляем опцию "All Projects" const allOption = document.createElement('div'); allOption.className = 'multi-select-option'; allOption.setAttribute('data-value', 'all'); allOption.innerHTML = ` `; dropdown.appendChild(allOption); // Добавляем проекты console.log('Adding projects to multi-select:', projects); projects.forEach(project => { const option = document.createElement('div'); option.className = 'multi-select-option'; option.setAttribute('data-value', project); option.innerHTML = ` `; dropdown.appendChild(option); }); // Восстанавливаем сохраненные выбранные проекты const savedProjects = JSON.parse(localStorage.getItem('lb_selected_projects') || '["all"]'); updateMultiSelect(savedProjects); console.log('Multi-select updated, current selection:', savedProjects); } else { console.error('Multi-select elements not found!'); } } catch (error) { console.error('Error fetching projects:', error); } } // Функция для обновления мультивыбора проектов function updateMultiSelect(selectedProjects) { const dropdown = document.getElementById('projectSelectDropdown'); const displayText = document.querySelector('.multi-select-text'); if (!dropdown || !displayText) return; // Фильтруем выбранные проекты, оставляя только те, которые есть в dropdown const availableProjects = []; dropdown.querySelectorAll('.multi-select-option').forEach(option => { const value = option.getAttribute('data-value'); availableProjects.push(value); }); const filteredProjects = selectedProjects.filter(project => project === 'all' || availableProjects.includes(project) ); // Если все выбранные проекты исчезли, выбираем "All Projects" if (filteredProjects.length === 0 || (filteredProjects.length === 1 && filteredProjects[0] === 'all')) { filteredProjects.length = 0; filteredProjects.push('all'); } // Обновляем чекбоксы dropdown.querySelectorAll('.multi-select-option').forEach(option => { const value = option.getAttribute('data-value'); const checkbox = option.querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.checked = filteredProjects.includes(value); } }); // Обновляем текст отображения if (filteredProjects.includes('all') || filteredProjects.length === 0) { displayText.textContent = 'All Projects'; } else if (filteredProjects.length === 1) { displayText.textContent = filteredProjects[0]; } else { displayText.textContent = `${filteredProjects.length} Projects`; } // Сохраняем в localStorage localStorage.setItem('lb_selected_projects', JSON.stringify(filteredProjects)); } // Функция для получения выбранных проектов function getSelectedProjects() { const dropdown = document.getElementById('projectSelectDropdown'); if (!dropdown) return ['all']; const selectedProjects = []; dropdown.querySelectorAll('.multi-select-option input[type="checkbox"]:checked').forEach(checkbox => { const option = checkbox.closest('.multi-select-option'); const value = option.getAttribute('data-value'); selectedProjects.push(value); }); return selectedProjects.length > 0 ? selectedProjects : ['all']; } // Функции для работы с исключенными контейнерами async function loadExcludedContainers() { try { const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); return []; } const response = await fetch('/api/containers/excluded', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { if (response.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return []; } console.error('Ошибка загрузки исключенных контейнеров:', response.status); return []; } const data = await response.json(); return data.excluded_containers || []; } catch (error) { console.error('Ошибка загрузки исключенных контейнеров:', error); return []; } } async function saveExcludedContainers(containers) { try { const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); return false; } const response = await fetch('/api/containers/excluded', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(containers) }); if (!response.ok) { if (response.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return false; } console.error('Ошибка сохранения исключенных контейнеров:', response.status); return false; } const data = await response.json(); console.log('Исключенные контейнеры сохранены:', data); return true; } catch (error) { console.error('Ошибка сохранения исключенных контейнеров:', error); return false; } } function renderExcludedContainers(containers) { const list = document.getElementById('excludedContainersList'); if (!list) return; list.innerHTML = ''; if (containers.length === 0) { list.innerHTML = '
Нет исключенных контейнеров
'; return; } containers.forEach(container => { const item = document.createElement('div'); item.className = 'excluded-container-item'; item.innerHTML = ` ${escapeHtml(container)} `; list.appendChild(item); }); } async function addExcludedContainer() { const input = document.getElementById('newExcludedContainer'); const containerName = input.value.trim(); if (!containerName) { alert('Введите имя контейнера'); return; } const currentContainers = await loadExcludedContainers(); if (currentContainers.includes(containerName)) { alert('Контейнер уже в списке исключенных'); return; } currentContainers.push(containerName); const success = await saveExcludedContainers(currentContainers); if (success) { renderExcludedContainers(currentContainers); input.value = ''; // Обновляем список проектов и контейнеров await fetchProjects(); await fetchServices(); } else { alert('Ошибка сохранения'); } } async function removeExcludedContainer(containerName) { const currentContainers = await loadExcludedContainers(); const updatedContainers = currentContainers.filter(name => name !== containerName); const success = await saveExcludedContainers(updatedContainers); if (success) { renderExcludedContainers(updatedContainers); // Обновляем список проектов и контейнеров await fetchProjects(); await fetchServices(); } else { alert('Ошибка удаления'); } } // Функции для мультивыбора контейнеров function toggleContainerSelection(containerId) { console.log('toggleContainerSelection called for:', containerId); console.log('Current state.selectedContainers before:', [...state.selectedContainers]); const index = state.selectedContainers.indexOf(containerId); if (index > -1) { state.selectedContainers.splice(index, 1); console.log('Removed container from selection:', containerId); } else { state.selectedContainers.push(containerId); console.log('Added container to selection:', containerId); } console.log('Current selected containers after:', state.selectedContainers); updateContainerSelectionUI(); updateMultiViewMode(); } function updateContainerSelectionUI() { console.log('updateContainerSelectionUI called, selected containers:', state.selectedContainers); // Обновляем чекбоксы и обычные карточки контейнеров const checkboxes = document.querySelectorAll('.container-checkbox'); console.log('Found checkboxes:', checkboxes.length); checkboxes.forEach(checkbox => { const containerId = checkbox.getAttribute('data-container-id'); const containerItem = checkbox.closest('.container-item'); console.log('Processing checkbox for container:', containerId, 'checked:', checkbox.checked, 'should be:', state.selectedContainers.includes(containerId)); if (state.selectedContainers.includes(containerId)) { checkbox.checked = true; containerItem.classList.add('selected'); console.log('Container selected:', containerId); } else { checkbox.checked = false; containerItem.classList.remove('selected'); console.log('Container deselected:', containerId); } }); // Обновляем миникарточки контейнеров const miniContainerItems = document.querySelectorAll('.mini-container-item'); miniContainerItems.forEach(miniItem => { const containerId = miniItem.getAttribute('data-cid'); if (state.selectedContainers.includes(containerId)) { miniItem.classList.add('selected'); } else { miniItem.classList.remove('selected'); } }); // Обновляем single-view-title если он существует const singleViewTitle = document.getElementById('singleViewTitle'); if (singleViewTitle && state.selectedContainers.length === 1) { const service = state.services.find(s => s.id === state.selectedContainers[0]); if (service) { singleViewTitle.textContent = `${service.name} (${service.service || service.name})`; } } else if (singleViewTitle && state.selectedContainers.length === 0) { singleViewTitle.textContent = 'No container selected'; } else if (singleViewTitle && state.selectedContainers.length > 1) { singleViewTitle.textContent = `Multi-view: ${state.selectedContainers.length} containers`; } // Если есть сохраненный контейнер, убеждаемся что его чекбокс отмечен const savedContainerId = getSelectedContainerFromStorage(); if (savedContainerId && state.selectedContainers.includes(savedContainerId)) { const checkbox = document.querySelector(`.container-checkbox[data-container-id="${savedContainerId}"]`); if (checkbox) { checkbox.checked = true; const containerItem = checkbox.closest('.container-item'); if (containerItem) { containerItem.classList.add('selected'); } } } // Обновляем видимость кнопок LogLevels updateLogLevelsVisibility(); } // Функция для обновления активного состояния контейнеров в UI function updateActiveContainerUI(activeContainerId) { console.log('updateActiveContainerUI called for:', activeContainerId); // Обновляем обычные карточки контейнеров const containerItems = document.querySelectorAll('.container-item'); containerItems.forEach(item => { const containerId = item.getAttribute('data-cid'); if (activeContainerId && containerId === activeContainerId) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // Обновляем миникарточки контейнеров const miniContainerItems = document.querySelectorAll('.mini-container-item'); miniContainerItems.forEach(miniItem => { const containerId = miniItem.getAttribute('data-cid'); if (activeContainerId && containerId === activeContainerId) { miniItem.classList.add('active'); } else { miniItem.classList.remove('active'); } }); // Обновляем legacy tabs const tabButtons = document.querySelectorAll('.tab'); tabButtons.forEach(tab => { const tabText = tab.textContent; const service = state.services.find(s => (s.project ? `[${s.project}] ` : '') + (s.service || s.name) === tabText ); if (service && activeContainerId && service.id === activeContainerId) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); } // Функции для всплывающих подсказок миникарточек function showMiniContainerTooltip(event, service) { // Удаляем существующую подсказку hideMiniContainerTooltip(); // Очищаем все таймеры скрытия const existingTooltips = document.querySelectorAll('.mini-container-tooltip'); existingTooltips.forEach(tooltip => { if (tooltip.hideTimer) { clearTimeout(tooltip.hideTimer); } }); // Создаем новую подсказку const tooltip = document.createElement('div'); tooltip.className = 'mini-container-tooltip'; tooltip.id = 'miniContainerTooltip'; const statusClass = service.status === 'running' ? 'running' : service.status === 'stopped' ? 'stopped' : 'paused'; tooltip.innerHTML = `
Контейнер
${escapeHtml(service.name)}
${escapeHtml(service.service || service.name)} • ${escapeHtml(service.project || 'standalone')}
${escapeHtml(service.status)}
${service.status === 'running' && service.host_port ? `
Порт: ${escapeHtml(service.host_port)}
` : ''} ${service.url ? ` Открыть сайт` : ''} `; // Добавляем подсказку в body document.body.appendChild(tooltip); // Позиционируем подсказку сразу positionTooltip(event, tooltip); // Показываем подсказку сразу tooltip.classList.add('show'); // Добавляем обработчики для подсказки tooltip.addEventListener('mouseenter', () => { // Останавливаем таймер скрытия при наведении на подсказку clearTimeout(tooltip.hideTimer); }); tooltip.addEventListener('mouseleave', () => { // Скрываем подсказку при уходе курсора с неё hideMiniContainerTooltip(); }); // Добавляем обработчик для клика по ссылке const link = tooltip.querySelector('.mini-container-tooltip-url'); if (link) { link.addEventListener('click', (e) => { e.stopPropagation(); // Ссылка откроется в новой вкладке благодаря target="_blank" }); } } function hideMiniContainerTooltip() { const tooltip = document.getElementById('miniContainerTooltip'); if (tooltip) { // Очищаем таймер скрытия if (tooltip.hideTimer) { clearTimeout(tooltip.hideTimer); } tooltip.remove(); } } function positionTooltip(event, tooltip) { const rect = event.target.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Определяем позицию по умолчанию (справа от миникарточки) let position = 'right'; let left = rect.right + 8; // Всегда центрируем по высоте относительно миникарточки let top = rect.top + (rect.height / 2) - (tooltipRect.height / 2); // Проверяем, помещается ли подсказка справа if (left + tooltipRect.width > viewportWidth - 10) { // Не помещается справа, пробуем слева position = 'left'; left = rect.left - tooltipRect.width - 8; } // Проверяем, помещается ли подсказка по вертикали if (top < 10) { // Не помещается сверху, выравниваем по верху с отступом top = 10; } else if (top + tooltipRect.height > viewportHeight - 10) { // Не помещается снизу, выравниваем по низу с отступом top = viewportHeight - tooltipRect.height - 10; } // Применяем позицию сразу, без анимации tooltip.className = `mini-container-tooltip ${position}`; tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; // Принудительно применяем стили без анимации tooltip.style.transition = 'none'; // Сбрасываем transition после применения позиции setTimeout(() => { tooltip.style.transition = ''; }, 10); } // Функции для help modal function showHelpTooltip() { const helpTooltip = document.getElementById('helpTooltip'); if (!helpTooltip) return; // Показываем модальное окно helpTooltip.classList.add('show'); // Блокируем скролл страницы document.body.style.overflow = 'hidden'; } function hideHelpTooltip() { const helpTooltip = document.getElementById('helpTooltip'); if (helpTooltip) { helpTooltip.classList.remove('show'); // Восстанавливаем скролл страницы document.body.style.overflow = ''; } } // Глобальные переменные для выбора контейнеров let lastSelectedContainerId = null; // Функция для диапазонного выбора контейнеров function selectContainerRange(startId, endId) { console.log('selectContainerRange:', startId, 'to', endId); const miniContainerItems = document.querySelectorAll('.mini-container-item'); const containerIds = Array.from(miniContainerItems).map(item => item.getAttribute('data-cid')); const startIndex = containerIds.indexOf(startId); const endIndex = containerIds.indexOf(endId); if (startIndex === -1 || endIndex === -1) { console.error('Container not found in range selection'); return; } const minIndex = Math.min(startIndex, endIndex); const maxIndex = Math.max(startIndex, endIndex); // Выбираем все контейнеры в диапазоне for (let i = minIndex; i <= maxIndex; i++) { const containerId = containerIds[i]; if (!state.selectedContainers.includes(containerId)) { state.selectedContainers.push(containerId); } } // Обновляем UI updateContainerSelectionUI(); updateMultiViewMode(); } // Глобальные обработчики для подсказок document.addEventListener('click', (event) => { const tooltip = document.getElementById('miniContainerTooltip'); if (tooltip && !tooltip.contains(event.target) && !event.target.closest('.mini-container-item')) { hideMiniContainerTooltip(); } }); window.addEventListener('resize', () => { hideMiniContainerTooltip(); hideHelpTooltip(); }); window.addEventListener('scroll', () => { hideMiniContainerTooltip(); hideHelpTooltip(); }); // Обработчик для скрытия подсказки при клике на ссылку document.addEventListener('click', (event) => { if (event.target.closest('.mini-container-tooltip-url')) { // Не скрываем подсказку при клике на ссылку event.stopPropagation(); } }); // Функция для сохранения выбранного контейнера в localStorage function saveSelectedContainer(containerId) { if (containerId) { localStorage.setItem('lb_selected_container', containerId); console.log('Saved selected container to localStorage:', containerId); } else { localStorage.removeItem('lb_selected_container'); console.log('Removed selected container from localStorage'); } } // Функция для восстановления выбранного контейнера из localStorage function getSelectedContainerFromStorage() { const containerId = localStorage.getItem('lb_selected_container'); console.log('Retrieved selected container from localStorage:', containerId); return containerId; } // Функция для сохранения режима просмотра в localStorage function saveViewMode(multiViewMode, selectedContainers) { const viewModeData = { multiViewMode: multiViewMode, selectedContainers: selectedContainers || [] }; localStorage.setItem('lb_view_mode', JSON.stringify(viewModeData)); console.log('Saved view mode to localStorage:', viewModeData); } // Функция для восстановления режима просмотра из localStorage function getViewModeFromStorage() { const viewModeData = localStorage.getItem('lb_view_mode'); if (viewModeData) { try { const data = JSON.parse(viewModeData); console.log('Retrieved view mode from localStorage:', data); return data; } catch (error) { console.error('Error parsing view mode from localStorage:', error); return null; } } return null; } // Функция для сохранения состояния кнопок loglevels в localStorage function saveLogLevelsState() { const levelsData = { globalLevels: state.levels, containerLevels: state.containerLevels }; localStorage.setItem('lb_log_levels', JSON.stringify(levelsData)); console.log('Saved log levels state to localStorage:', levelsData); } // Функция для восстановления состояния кнопок loglevels из localStorage function getLogLevelsStateFromStorage() { const levelsData = localStorage.getItem('lb_log_levels'); if (levelsData) { try { const data = JSON.parse(levelsData); console.log('Retrieved log levels state from localStorage:', data); return data; } catch (error) { console.error('Error parsing log levels state from localStorage:', error); return null; } } return null; } async 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'); // Сохраняем режим просмотра в localStorage saveViewMode(true, state.selectedContainers); await setupMultiView(); } else if (state.selectedContainers.length === 1) { // Переключаемся в single view для одного контейнера console.log('Switching from multi-view to single view'); state.multiViewMode = false; const selectedService = state.services.find(s => s.id === state.selectedContainers[0]); if (selectedService) { console.log('Switching to single view for:', selectedService.name); console.log('updateMultiViewMode: About to call switchToSingle - VERSION 2'); // Сохраняем режим просмотра в localStorage saveViewMode(false, [selectedService.id]); // Сохраняем выбранный контейнер в localStorage saveSelectedContainer(selectedService.id); // Обновляем страницу для полного сброса состояния console.log('Refreshing page to switch to single view'); window.location.reload(); return; // Прерываем выполнение, так как страница перезагрузится } } else { // Когда снимаем все галочки, переключаемся в single view state.multiViewMode = false; state.current = null; // Сохраняем режим просмотра в localStorage saveViewMode(false, []); clearLogArea(); // Очищаем область логов и показываем пустое состояние const logArea = document.querySelector('.log-area'); if (logArea) { const logContent = logArea.querySelector('.log-content'); if (logContent) { logContent.innerHTML = '
Выберите контейнер для просмотра логов
'; } } // Очищаем активное состояние всех контейнеров updateActiveContainerUI(null); } console.log(`Multi-view mode updated: multiViewMode = ${state.multiViewMode}`); // Сохраняем состояние кнопок loglevels при переключении режимов saveLogLevelsState(); // Обновляем состояние кнопок уровней логирования при переключении режимов setTimeout(() => { initializeLevelButtons(); }, 100); // Обновляем видимость кнопок LogLevels updateLogLevelsVisibility(); } /** * Настраивает интерфейс для режима мультипросмотра (multi-view) * Создает сетку панелей для одновременного просмотра нескольких контейнеров * Открывает WebSocket соединения для всех выбранных контейнеров */ async function setupMultiView() { console.log('setupMultiView called'); // Проверяем, что у нас действительно больше одного контейнера if (state.selectedContainers.length <= 1) { console.log('setupMultiView: Not enough containers for multi-view, switching to single view'); if (state.selectedContainers.length === 1) { const selectedService = state.services.find(s => s.id === state.selectedContainers[0]); if (selectedService) { console.log('setupMultiView: Calling switchToSingle for:', selectedService.name); await switchToSingle(selectedService); } } else { console.log('setupMultiView: No containers selected, clearing log area'); clearLogArea(); } return; } // Дополнительная проверка - если уже есть мультипросмотр, удаляем его для пересоздания const existingMultiView = document.getElementById('multiViewGrid'); if (existingMultiView) { console.log('setupMultiView: Multi-view already exists, removing for recreation'); existingMultiView.remove(); } 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 = ''; } // Удаляем single-view-panel если он существует const singleViewPanel = document.getElementById('singleViewPanel'); if (singleViewPanel) { singleViewPanel.remove(); } // Создаем сетку для мультипросмотра const gridContainer = document.createElement('div'); gridContainer.className = 'multi-view-grid'; gridContainer.id = 'multiViewGrid'; // Определяем количество колонок в зависимости от количества контейнеров let columns = 1; if (state.selectedContainers.length === 1) columns = 1; else 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; console.log(`setupMultiView: Creating grid with ${columns} columns for ${state.selectedContainers.length} containers`); gridContainer.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; console.log(`setupMultiView: Grid template columns set to: repeat(${columns}, 1fr)`); // Создаем панели для каждого выбранного контейнера console.log(`setupMultiView: Creating panels for ${state.selectedContainers.length} containers:`, state.selectedContainers); state.selectedContainers.forEach((containerId, index) => { const service = state.services.find(s => s.id === containerId); if (!service) { console.error(`setupMultiView: Service not found for container ID: ${containerId}`); return; } console.log(`setupMultiView: Creating panel ${index + 1} for service: ${service.name} (${containerId})`); const panel = createMultiViewPanel(service); gridContainer.appendChild(panel); console.log(`setupMultiView: Panel ${index + 1} added to grid, total children: ${gridContainer.children.length}`); }); if (logContent) { logContent.appendChild(gridContainer); console.log(`setupMultiView: Grid added to log content, grid children: ${gridContainer.children.length}`); // Проверяем, что все панели созданы правильно const panels = gridContainer.querySelectorAll('.multi-view-panel'); console.log(`setupMultiView: Total panels found in grid: ${panels.length}`); panels.forEach((panel, index) => { const containerId = panel.getAttribute('data-container-id'); const title = panel.querySelector('.multi-view-title'); console.log(`setupMultiView: Panel ${index + 1}: containerId=${containerId}, title="${title?.textContent}"`); }); } else { console.error('setupMultiView: logContent not found'); } // Применяем настройки wrap lines applyWrapSettings(); // Очищаем активное состояние всех контейнеров в мультипросмотре updateActiveContainerUI(null); // Принудительно обновляем стили логов для multi-view setTimeout(() => { updateLogStyles(); // Дополнительная проверка для multi-view логов console.log('setupMultiView: Force fixing multi-view styles'); forceFixMultiViewStyles(); }, 200); // Подключаем WebSocket для каждого контейнера console.log(`setupMultiView: Setting up WebSockets for ${state.selectedContainers.length} containers`); state.selectedContainers.forEach((containerId, index) => { const service = state.services.find(s => s.id === containerId); if (service) { console.log(`setupMultiView: Setting up WebSocket ${index + 1} for multi-view container: ${service.name} (${containerId})`); openMultiViewWs(service); } else { console.error(`setupMultiView: Service not found for container ID: ${containerId}`); } }); console.log(`setupMultiView: Multi-view setup completed for ${state.selectedContainers.length} containers`); // Обновляем счетчики для multi view setTimeout(() => { recalculateMultiViewCounters(); }, 1000); // Небольшая задержка для завершения загрузки логов // Применяем стили логов после настройки multi view setTimeout(() => { updateLogStyles(); }, 1500); // Задержка после настройки счетчиков // Обновляем видимость кнопок LogLevels updateLogLevelsVisibility(); } /** * Создает панель для мультипросмотра контейнера * Генерирует HTML структуру с заголовком, кнопками уровней и областью логов * @param {Object} service - Объект сервиса/контейнера * @returns {HTMLElement} Созданная панель мультипросмотра */ 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); console.log(`createMultiViewPanel: Panel element created with data-container-id: ${service.id}`); panel.innerHTML = `

${escapeHtml(service.name)}

`; // Проверяем, что элемент создался правильно 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}`); } // Инициализируем состояние кнопок уровней логирования для этого контейнера setTimeout(() => { const levelBtns = panel.querySelectorAll('.level-btn'); levelBtns.forEach(btn => { const level = btn.getAttribute('data-level'); const containerId = btn.getAttribute('data-container-id'); // Инициализируем настройки контейнера, если их нет if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) { if (!state.containerLevels) { state.containerLevels = {}; } state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true}; } // Используем настройки контейнера const isActive = state.containerLevels && state.containerLevels[containerId] ? state.containerLevels[containerId][level] : true; btn.classList.toggle('active', isActive); btn.classList.toggle('disabled', !isActive); }); }, 100); console.log(`Multi-view panel created for ${service.name}`); // Применяем стили к новой панели setTimeout(() => { updateLogStyles(); }, 200); return panel; } /** * Открывает WebSocket соединение для контейнера в режиме мультипросмотра * Настраивает обработчики сообщений и управляет отображением логов * @param {Object} service - Объект сервиса/контейнера */ function openMultiViewWs(service) { const containerId = service.id; console.log(`openMultiViewWs: Starting WebSocket setup for ${service.name} (${containerId})`); console.log(`openMultiViewWs: Current multiViewMode: ${state.multiViewMode}`); console.log(`openMultiViewWs: Selected containers: ${state.selectedContainers.join(', ')}`); // Закрываем существующее соединение 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) { // Убираем сообщение "Connected..." для MultiView режима logEl.textContent = ''; // Очищаем пустые строки после установки соединения setTimeout(() => { cleanMultiViewEmptyLines(logEl); cleanMultiViewDuplicateLines(logEl); }, 100); } }; ws.onmessage = (event) => { console.log(`Multi-view WebSocket received message for ${service.name}: ${event.data.substring(0, 100)}...`); // Устанавливаем состояние 'on' при получении сообщений setWsState('on'); const parts = (event.data||'').split(/\r?\n/); // Проверяем на дублирование в исходных данных if (event.data.includes('FoundINFO:')) { console.log('🚨 WebSocket: ОБНАРУЖЕНЫ данные с FoundINFO:'); console.log('🚨 WebSocket: Полные данные:', event.data); } // Проверяем на дублирование строк и убираем дубликаты const lines = event.data.split(/\r?\n/).filter(line => 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 { console.log(`Multi-view WebSocket closed for ${service.name}`); }; ws.onerror = (error) => { console.error(`Multi-view WebSocket error for ${service.name}:`, error); }; // Сохраняем соединение с полным набором полей как в openWs const logEl = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); console.log(`openMultiViewWs: Found log element for ${service.name}:`, !!logEl); state.open[containerId] = { ws: ws, serviceName: service.service, logEl: logEl, wrapEl: logEl, counters: {dbg:0, info:0, warn:0, err:0, other:0}, pausedBuffer: [], allLogs: [] // Добавляем буфер для логов }; console.log(`openMultiViewWs: WebSocket setup completed for ${service.name} (${containerId})`); } function clearLogArea() { console.log('clearLogArea called'); // Очищаем мультипросмотр если он был активен if (state.multiViewMode) { console.log('Clearing multi-view grid'); const multiViewGrid = document.getElementById('multiViewGrid'); if (multiViewGrid) { multiViewGrid.remove(); } } const logContent = document.querySelector('.log-content'); if (logContent) { logContent.innerHTML = `

No container selected

No container selected
`; } const logTitle = document.getElementById('logTitle'); if (logTitle) { logTitle.textContent = 'LogBoard+'; } // Обновляем видимость кнопок LogLevels updateLogLevelsVisibility(); } function applyWrapSettings() { const wrapEnabled = els.wrapToggle && els.wrapToggle.checked; const wrapStyle = wrapEnabled ? 'pre-wrap' : 'pre'; // Применяем к обычному просмотру (только к логам в основном контенте) document.querySelectorAll('.main-content .log').forEach(el => { el.style.whiteSpace = wrapStyle; }); // Применяем к мультипросмотру document.querySelectorAll('.multi-view-content .multi-view-log').forEach(el => { el.style.whiteSpace = wrapStyle; }); // Применяем к single-view document.querySelectorAll('.single-view-content .log').forEach(el => { el.style.whiteSpace = wrapStyle; }); } async function fetchServices(){ try { console.log('Fetching services...'); const url = new URL(location.origin + '/api/containers/services'); const selectedProjects = getSelectedProjects(); // Если выбраны конкретные проекты (не "all"), добавляем их в URL как строку через запятую if (selectedProjects.length > 0 && !selectedProjects.includes('all')) { url.searchParams.set('projects', selectedProjects.join(',')); } const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); window.location.href = '/login'; return; } const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); if (!res.ok){ if (res.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return; } console.error('Auth failed (HTTP):', res.status, res.statusText); alert('Auth failed (HTTP)'); return; } const data = await res.json(); console.log('Services loaded:', data); state.services = data; buildTabs(); // Восстанавливаем режим просмотра из localStorage const savedViewMode = getViewModeFromStorage(); if (savedViewMode) { console.log('Restoring view mode from localStorage:', savedViewMode); if (savedViewMode.multiViewMode && savedViewMode.selectedContainers.length > 1) { // Восстанавливаем Multi View режим console.log('Restoring Multi View mode with containers:', savedViewMode.selectedContainers); state.multiViewMode = true; state.selectedContainers = savedViewMode.selectedContainers; // Отмечаем чекбоксы для выбранных контейнеров savedViewMode.selectedContainers.forEach(containerId => { const checkbox = document.querySelector(`.container-checkbox[data-container-id="${containerId}"]`); if (checkbox) { checkbox.checked = true; const containerItem = checkbox.closest('.container-item'); if (containerItem) { containerItem.classList.add('selected'); } } }); // Настраиваем Multi View await setupMultiView(); } else if (savedViewMode.selectedContainers.length === 1) { // Восстанавливаем Single View режим console.log('Restoring Single View mode for container:', savedViewMode.selectedContainers[0]); state.multiViewMode = false; const selectedService = state.services.find(s => s.id === savedViewMode.selectedContainers[0]); if (selectedService) { await switchToSingle(selectedService); } } else { // Нет сохраненного режима, не открываем автоматически первый контейнер console.log('No saved view mode, not auto-opening first container'); // Пользователь сам выберет нужный контейнер } } else { // Нет сохраненного режима, не открываем автоматически первый контейнер console.log('No saved view mode found, not auto-opening first container'); // Пользователь сам выберет нужный контейнер } // Добавляем обработчики для счетчиков после загрузки сервисов addCounterClickHandlers(); } catch (error) { console.error('Error fetching services:', error); } } function wsUrl(containerId, service, project){ const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const tail = els.tail.value || '500'; const token = encodeURIComponent(localStorage.getItem('access_token') || ''); const sp = service?`&service=${encodeURIComponent(service)}`:''; const pj = project?`&project=${encodeURIComponent(project)}`:''; if (els.aggregate && els.aggregate.checked && service){ // fan-in by service return `${proto}://${location.host}/api/websocket/fan/${encodeURIComponent(service)}?tail=${tail}&token=${token}${pj}`; } return `${proto}://${location.host}/api/websocket/logs/${encodeURIComponent(containerId)}?tail=${tail}&token=${token}${sp}${pj}`; } /** * Закрывает WebSocket соединение для контейнера * @param {string} id - ID контейнера */ function closeWs(id){ const o = state.open[id]; if (!o) return; try { o.ws.close(); } catch(e){} delete state.open[id]; } /** * Создает и скачивает снимок логов контейнера * В режиме мультипросмотра создает отдельные файлы для каждого контейнера * @param {string} id - ID контейнера */ async function sendSnapshot(id){ const o = state.open[id]; if (!o){ alert('not open'); return; } const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); window.location.href = '/login'; return; } if (state.multiViewMode && state.selectedContainers.length > 0) { // В Multi View режиме создаем отдельный файл для каждого контейнера console.log('Creating snapshots for Multi View mode with containers:', state.selectedContainers); let hasLogs = false; // Создаем отдельный файл для каждого выбранного контейнера for (const containerId of state.selectedContainers) { const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (multiViewLog && multiViewLog.textContent.trim()) { const service = state.services.find(s => s.id === containerId); const serviceName = service ? (service.service || service.name) : containerId; const text = multiViewLog.textContent; console.log(`Saving snapshot for ${serviceName} with content length:`, text.length); const payload = {container_id: containerId, service: serviceName, content: text}; try { const res = await fetch('/api/logs/snapshot', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); if (!res.ok){ if (res.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return; } console.error(`Snapshot failed for ${serviceName}:`, res.status, res.statusText); alert(`snapshot failed for ${serviceName}`); return; } const js = await res.json(); const a = document.createElement('a'); a.href = js.url; a.download = js.file; a.click(); hasLogs = true; // Небольшая задержка между скачиваниями файлов await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { console.error(`Error saving snapshot for ${serviceName}:`, error); alert(`Error saving snapshot for ${serviceName}`); return; } } } if (!hasLogs) { alert('No logs to save in Multi View mode'); return; } } else { // Обычный режим просмотра let text = ''; if (state.current && state.current.id === id && els.logContent) { text = els.logContent.textContent; } else if (o.logEl) { text = o.logEl.textContent; } if (!text || text.trim() === '') { alert('No logs to save'); return; } console.log('Saving snapshot with content length:', text.length); const serviceName = o.serviceName || id; const payload = {container_id: id, service: serviceName, content: text}; const res = await fetch('/api/logs/snapshot', { method:'POST', headers:{ 'Content-Type':'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); if (!res.ok){ if (res.status === 401) { console.error('Unauthorized, redirecting to login'); window.location.href = '/login'; return; } console.error('Snapshot failed:', res.status, res.statusText); alert('snapshot failed'); return; } const js = await res.json(); const a = document.createElement('a'); a.href = js.url; a.download = js.file; a.click(); } } /** * Открывает WebSocket соединение для контейнера * Настраивает обработчики событий и управляет отображением логов * @param {Object} svc - Объект сервиса/контейнера * @param {HTMLElement} panel - Панель для отображения логов */ function openWs(svc, panel){ const id = svc.id; console.log(`openWs: Called for ${svc.name} (${id}) in multiViewMode: ${state.multiViewMode}`); console.log(`openWs: Selected containers: ${state.selectedContainers.join(', ')}`); const logEl = panel.querySelector('.log'); const wrapEl = panel.querySelector('.logwrap'); // Ищем счетчики в panel или в глобальных элементах const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg'); const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo'); const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn'); const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr'); const cother = panel.querySelector('.cother') || document.querySelector('.cother'); const counters = {dbg:0,info:0,warn:0,err:0,other:0}; const ws = new WebSocket(wsUrl(id, (svc.service||svc.name), svc.project||'')); state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: (svc.service||svc.name)}; console.log(`openWs: Created state.open[${id}] with logEl:`, !!logEl, 'wrapEl:', !!wrapEl); ws.onopen = ()=> { console.log(`WebSocket ${id}: Соединение открыто`); setWsState('on'); // Очищаем сообщение "Connecting..." когда соединение установлено if (state.current && state.current.id === id && els.logContent) { els.logContent.innerHTML = ''; } // Также очищаем legacy элемент лога if (obj.logEl) { obj.logEl.innerHTML = ''; } // Принудительно проверяем состояние через AJAX через 500мс и 1 секунду setTimeout(() => { console.log(`WebSocket ${id}: Принудительная проверка состояния после открытия (500мс)`); checkWebSocketStatus(); }, 500); setTimeout(() => { console.log(`WebSocket ${id}: Принудительная проверка состояния после открытия (1с)`); checkWebSocketStatus(); }, 1000); }; ws.onclose = ()=> { console.log(`WebSocket ${id}: Соединение закрыто`); setWsState(determineWsState()); // Принудительно проверяем состояние через AJAX через 500мс setTimeout(() => { console.log(`WebSocket ${id}: Принудительная проверка состояния после закрытия`); checkWebSocketStatus(); }, 500); }; ws.onerror = (error)=> { console.log(`WebSocket ${id}: Ошибка соединения:`, error); setWsState('err'); }; ws.onmessage = (ev)=>{ console.log(`Received WebSocket message: ${ev.data.substring(0, 100)}...`); // Устанавливаем состояние 'on' при получении сообщений setWsState('on'); const parts = (ev.data||'').split(/\r?\n/); console.log(`openWs: Processing ${parts.length} lines for container ${id}`); // Проверяем на дублирование в исходных данных для Single View if (ev.data.includes('FoundINFO:')) { console.log('🚨 Single View WebSocket: ОБНАРУЖЕНЫ данные с FoundINFO:'); console.log('🚨 Single View WebSocket: Полные данные:', ev.data); } // Проверяем на дублирование строк и убираем дубликаты const lines = ev.data.split(/\r?\n/).filter(line => 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(); removedCount++; } }); // Удаляем все текстовые узлы, которые содержат только пробелы и переносы строк const walker = document.createTreeWalker( multiViewLog, NodeFilter.SHOW_TEXT, null, false ); const textNodesToRemove = []; let node; while (node = walker.nextNode()) { const content = node.textContent; // Удаляем все узлы, которые содержат только пробелы, переносы строк или табуляцию if (content.trim() === '') { textNodesToRemove.push(node); } } 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} пустых строк`); } } /** * Функция для очистки дублированных строк в 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()) { console.log(`cleanMultiViewDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); currentLine.remove(); removedCount++; } } } // После удаления дубликатов очищаем лишние пустые строки cleanMultiViewEmptyLines(multiViewLog); 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()) { console.log(`cleanDuplicateLines: Удаляем дублированную строку: ${currentText.substring(0, 50)}...`); currentLine.remove(); removedCount++; } } } // После удаления дубликатов очищаем лишние пустые строки if (logElement.classList.contains('multi-view-log')) { cleanMultiViewEmptyLines(logElement); } else { cleanSingleViewEmptyLines(logElement); } if (removedCount > 0) { console.log(`cleanDuplicateLines: Удалено ${removedCount} дублированных строк`); } } /** * Функция для радикальной очистки пустых строк в Single View * Удаляет все пустые строки и лишние переносы строк * @param {HTMLElement} logElement - элемент лога Single View */ function cleanSingleViewEmptyLines(logElement) { if (!logElement) return; let removedCount = 0; // Удаляем все пустые строки (элементы .line без текста) const lines = Array.from(logElement.querySelectorAll('.line')); lines.forEach(line => { const textContent = line.textContent || line.innerText || ''; if (textContent.trim() === '') { line.remove(); removedCount++; } }); // Удаляем все текстовые узлы, которые содержат только пробелы и переносы строк const walker = document.createTreeWalker( logElement, NodeFilter.SHOW_TEXT, null, false ); const textNodesToRemove = []; let node; while (node = walker.nextNode()) { const content = node.textContent; // Удаляем все узлы, которые содержат только пробелы, переносы строк или табуляцию if (content.trim() === '') { textNodesToRemove.push(node); } } 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) { console.log(`cleanSingleViewEmptyLines: Удалено ${removedCount} пустых строк`); } } /** * Функция для нормализации пробелов в логах * Заменяет множественные пробелы на один пробел * @param {string} text - исходный текст * @returns {string} - текст с нормализованными пробелами */ function normalizeSpaces(text) { if (!text) return text; // Заменяем множественные пробелы на один пробел // Используем регулярное выражение для замены 2+ пробелов на один 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); // Запускаем периодическую проверку стилей multi-view логов каждые 5 секунд setInterval(() => { if (state.multiViewMode) { const multiViewLogs = document.querySelectorAll('.multi-view-content .multi-view-log'); if (multiViewLogs.length > 0) { console.log('Periodic check: Force fixing multi-view styles'); forceFixMultiViewStyles(); // Дополнительно исправляем все контейнеры console.log('Periodic check: Fixing all containers'); if (window.fixAllContainers) { window.fixAllContainers(); } } } }, 5000); /** * Функция для обработки специальных замен в 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 ==='); } /** * Основная функция обработки строк логов * Классифицирует, фильтрует и отображает строки логов в зависимости от режима * @param {string} id - ID контейнера * @param {string} line - Строка лога для обработки */ 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, other:0}; } // Фильтруем сообщение "Connected to container" для всех режимов // Это сообщение отправляется сервером при установке WebSocket соединения if (line.includes('Connected to container:')) { console.log(`handleLine: Фильтруем сообщение "Connected to container" для контейнера ${id}`); return; // Пропускаем это сообщение во всех режимах } // Нормализуем пробелы в строке лога const normalizedLine = normalizeSpaces(line); const cls = classify(normalizedLine); // Обновляем счетчики только для отображаемых логов // Проверяем фильтры для отображения в зависимости от режима let shouldShow; if (state.multiViewMode && state.selectedContainers.includes(id)) { // Для multi-view используем настройки конкретного контейнера shouldShow = allowedByContainerLevel(cls, id) && applyFilter(normalizedLine); } else { // Для single-view используем глобальные настройки 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++; if (cls==='other') obj.counters.other++; } // Обновляем счетчики в кнопках заголовков updateHeaderCounters(id, obj.counters); // Для Single View НЕ добавляем перенос строки после каждой строки лога const html = `${ansiToHtml(normalizedLine)}`; // Сохраняем все логи в буфере (всегда) if (!obj.allLogs) obj.allLogs = []; // Для Single View сохраняем обработанную строку, для MultiView - оригинальную const processedLine = !state.multiViewMode ? processSingleViewSpecialReplacements(normalizedLine) : normalizedLine; const processedHtml = `${ansiToHtml(processedLine)}`; obj.allLogs.push({html: processedHtml, line: processedLine, cls: cls}); // Ограничиваем размер буфера if (obj.allLogs.length > 10000) { obj.allLogs = obj.allLogs.slice(-5000); } // Добавляем логи в отображение (обычный просмотр) - только если НЕ в 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)}`; obj.logEl.insertAdjacentHTML('beforeend', singleViewHtml); // Очищаем лишние пустые строки в Single View cleanSingleViewEmptyLines(obj.logEl); if (els.autoscroll && els.autoscroll.checked && obj.wrapEl) { obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; } // 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:`, singleViewHtml.substring(0, 100)); // Добавляем новую строку напрямую в современный интерфейс 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; } } else { console.log(`handleLine: Modern interface update skipped - state.current:`, state.current?.id, `id:`, id, `logContent exists:`, !!els.logContent); } } // Update multi-view interface if (state.multiViewMode && state.selectedContainers.includes(id)) { const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${id}"]`); if (multiViewLog) { // Проверяем фильтры для конкретного контейнера const shouldShowInMultiView = allowedByContainerLevel(cls, id) && applyFilter(normalizedLine); if (shouldShowInMultiView) { // Применяем ограничение 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)}`; // Добавляем новую строку multiViewLog.insertAdjacentHTML('beforeend', multiViewHtml); // Очищаем пустые строки в multi view cleanMultiViewEmptyLines(multiViewLog); // Очищаем дублированные строки в multi view cleanMultiViewDuplicateLines(multiViewLog); // Ограничиваем количество отображаемых строк const logLines = Array.from(multiViewLog.querySelectorAll('.line')); if (logLines.length > tailLines) { // Удаляем лишние строки с начала const linesToRemove = logLines.length - tailLines; console.log(`handleLine: Trimming ${linesToRemove} lines from container ${id} (tail: ${tailLines})`); // Удаляем первые N строк logLines.slice(0, linesToRemove).forEach(line => { line.remove(); }); } if (els.autoscroll && els.autoscroll.checked) { multiViewLog.scrollTop = multiViewLog.scrollHeight; } console.log(`handleLine: Updated multi-view for container ${id}, log element found: true, tail lines: ${tailLines}`); } } else { console.error(`handleLine: Multi-view log element not found for container ${id}`); } // Обновляем счетчики в multi view периодически (каждые 10 строк) 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); // Обновляем каждую секунду } } } function ensurePanel(svc){ let panel = els.grid.querySelector(`.panel[data-cid="${svc.id}"]`); if (!panel){ panel = panelTemplate(svc); els.grid.appendChild(panel); panel.querySelector('.t-reconnect').onclick = ()=>{ const id = svc.id; const o = state.open[id]; if (o){ o.logEl.textContent=''; closeWs(id); } openWs(svc, panel); }; panel.querySelector('.t-close').onclick = ()=>{ closeWs(svc.id); panel.remove(); if (!Object.keys(state.open).length) setWsState('off'); }; panel.querySelector('.t-snapshot').onclick = ()=> sendSnapshot(svc.id); } return panel; } /** * Переключает интерфейс в режим одиночного просмотра (single view) * Закрывает мультипросмотр, открывает WebSocket для выбранного контейнера * @param {Object} svc - Объект сервиса/контейнера для просмотра */ async function switchToSingle(svc){ console.log('switchToSingle: ENTRY POINT - function called - VERSION 2'); console.log('switchToSingle: svc parameter:', svc); try { console.log('switchToSingle called for:', svc.name); console.log('switchToSingle: Starting function execution'); console.log('switchToSingle: svc name:', svc.name, 'id:', svc.id); console.log('switchToSingle: state.current:', state.current?.name, 'multiViewMode:', state.multiViewMode); // Всегда очищаем мультипросмотр при переключении в single view console.log('Clearing multi-view mode'); state.multiViewMode = false; // Закрываем WebSocket соединения для мультипросмотра console.log('switchToSingle: Closing WebSocket connections for multi-view'); state.selectedContainers.forEach(containerId => { closeWs(containerId); }); // Очищаем область логов console.log('switchToSingle: Clearing log content'); if (els.logContent) { els.logContent.innerHTML = ''; } // Воссоздаем single-view-panel если его нет const logContent = document.querySelector('.log-content'); const singleViewPanel = document.getElementById('singleViewPanel'); if (logContent && !singleViewPanel) { console.log('switchToSingle: Recreating single-view-panel'); logContent.innerHTML = `

${svc.name} (${svc.service || svc.name})

Connecting...
`; // Обновляем ссылки на элементы els.singleViewPanel = document.getElementById('singleViewPanel'); els.singleViewTitle = document.getElementById('singleViewTitle'); els.logContent = document.getElementById('logContent'); } // Удаляем мультипросмотр из DOM const multiViewGrid = document.getElementById('multiViewGrid'); if (multiViewGrid) { console.log('Removing multi-view grid from DOM'); multiViewGrid.remove(); } else { console.log('Multi-view grid not found in DOM'); } // Legacy functionality (скрытая) console.log('switchToSingle: Setting up legacy functionality'); setLayout('tabs'); els.grid.innerHTML=''; const panel = ensurePanel(svc); panel.querySelector('.log').textContent=''; closeWs(svc.id); console.log('switchToSingle: Calling openWs for:', svc.name, 'id:', svc.id); openWs(svc, panel); state.current = svc; console.log('switchToSingle: Set state.current to:', svc.name, 'id:', svc.id); console.log('switchToSingle: state.current after setting:', state.current); buildTabs(); for (const p of [...els.grid.children]) if (p!==panel) p.remove(); // Обновляем состояние выбранных контейнеров для корректного отображения заголовка state.selectedContainers = [svc.id]; // Сохраняем режим просмотра в localStorage saveViewMode(false, [svc.id]); // Обновляем активное состояние в UI updateActiveContainerUI(svc.id); // Сохраняем состояние кнопок loglevels в localStorage saveLogLevelsState(); if (els.multiViewPanelTitle) { els.multiViewPanelTitle.textContent = `${svc.name} (${svc.service || svc.name})`; } if (els.singleViewTitle) { els.singleViewTitle.textContent = `${svc.name} (${svc.service || svc.name})`; } if (els.logContent) { els.logContent.innerHTML = 'Connecting...'; } // Обновляем logEl для современного интерфейса const obj = state.open[svc.id]; if (obj && els.logContent) { obj.logEl = els.logContent; obj.wrapEl = els.logContent.parentElement; console.log('switchToSingle: Updated obj.logEl and obj.wrapEl for modern interface'); // Если у нас уже есть логи в буфере, отображаем их if (obj.allLogs && obj.allLogs.length > 0) { console.log(`switchToSingle: Restoring ${obj.allLogs.length} buffered log lines`); els.logContent.innerHTML = ''; obj.allLogs.forEach(logEntry => { if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) { els.logContent.insertAdjacentHTML('beforeend', logEntry.html); } }); // Очищаем лишние пустые строки после восстановления логов cleanSingleViewEmptyLines(els.logContent); cleanDuplicateLines(els.logContent); if (els.autoscroll && els.autoscroll.checked) { els.logContent.scrollTop = els.logContent.scrollHeight; } } } // Update active state in container list document.querySelectorAll('.container-item').forEach(item => { item.classList.remove('active'); }); const activeItem = document.querySelector(`.container-item[data-cid="${svc.id}"]`); if (activeItem) { activeItem.classList.add('active'); } // Обновляем состояние чекбоксов после переключения контейнера updateContainerSelectionUI(); // Обновляем счетчики для нового контейнера setTimeout(() => { recalculateCounters(); // Применяем настройки wrap text после переключения контейнера applyWrapSettings(); }, 500); // Небольшая задержка для завершения загрузки логов await updateCounters(svc.id); // Добавляем обработчики для счетчиков после переключения контейнера addCounterClickHandlers(); // Обновляем состояние кнопок уровней логирования setTimeout(() => { initializeLevelButtons(); }, 100); } catch (error) { console.error('switchToSingle: Error occurred:', error); console.error('switchToSingle: Error stack:', error.stack); } } async function openMulti(ids){ els.grid.innerHTML=''; const chosen = state.services.filter(s=> ids.includes(s.id)); const n = chosen.length; if (n<=1){ if (n===1) await switchToSingle(chosen[0]); return; } setLayout(n>=4 ? 'grid4' : n===3 ? 'grid3' : 'grid2'); for (const svc of chosen){ const panel = ensurePanel(svc); panel.querySelector('.log').textContent=''; closeWs(svc.id); openWs(svc, panel); } // Добавляем обработчики для счетчиков после открытия мульти-контейнеров addCounterClickHandlers(); // Применяем настройки wrap text после открытия мульти-контейнеров applyWrapSettings(); } // ----- Copy on selection ----- function getSelectionText(){ const sel = window.getSelection(); return sel && sel.rangeCount ? sel.toString() : ""; } function showCopyFabNearSelection(){ const sel = window.getSelection(); if (!sel || sel.rangeCount===0) return hideCopyFab(); const text = sel.toString(); if (!text.trim()) return hideCopyFab(); // Only show if selection inside a .log or .logwrap const range = sel.getRangeAt(0); const common = range.commonAncestorContainer; const el = common.nodeType===1 ? common : common.parentElement; if (!el || !el.closest('.logwrap')) return hideCopyFab(); const rect = range.getBoundingClientRect(); const top = rect.bottom + 8 + window.scrollY; const left = rect.right + 8 + window.scrollX; els.copyFab.style.top = top + 'px'; els.copyFab.style.left = left + 'px'; els.copyFab.classList.add('show'); } function hideCopyFab(){ els.copyFab.classList.remove('show'); } document.addEventListener('selectionchange', ()=>{ // throttle-ish using requestAnimationFrame window.requestAnimationFrame(showCopyFabNearSelection); }); document.addEventListener('scroll', hideCopyFab, true); els.copyFab.addEventListener('click', async ()=>{ const text = getSelectionText(); if (!text) return; try { await navigator.clipboard.writeText(text); const old = els.copyFab.textContent; els.copyFab.textContent = 'скопировано'; setTimeout(()=> els.copyFab.textContent = old, 1000); hideCopyFab(); window.getSelection()?.removeAllRanges(); } catch(e){ alert('не удалось скопировать: ' + e); } }); function fanGroupUrl(servicesCsv, project){ const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const tail = els.tail.value || '500'; const token = encodeURIComponent(localStorage.getItem('access_token') || ''); const pj = project?`&project=${encodeURIComponent(project)}`:''; return `${proto}://${location.host}/api/websocket/fan_group?services=${encodeURIComponent(servicesCsv)}&tail=${tail}&token=${token}${pj}`; } function openFanGroup(services){ // Build a special panel named after the group els.grid.innerHTML=''; const fake = { id: 'group-'+services.join(','), name:'group', service: 'group', project: (localStorage.lb_project||'') }; const panel = ensurePanel(fake); panel.querySelector('.log').textContent=''; closeWs(fake.id); // Override ws creation to fan_group const id = fake.id; const logEl = panel.querySelector('.log'); const wrapEl = panel.querySelector('.logwrap'); const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg'); const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo'); const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn'); const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr'); const cother = panel.querySelector('.cother') || document.querySelector('.cother'); const counters = {dbg:0,info:0,warn:0,err:0,other:0}; const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||'')); state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: ('group:'+services.join(','))}; ws.onopen = ()=> { console.log(`WebSocket ${id}: Соединение открыто`); setWsState('on'); }; ws.onclose = ()=> { console.log(`WebSocket ${id}: Соединение закрыто`); setWsState(determineWsState()); // Принудительно проверяем состояние через AJAX через 500мс setTimeout(() => { console.log(`WebSocket ${id}: Принудительная проверка состояния после закрытия`); checkWebSocketStatus(); }, 500); }; ws.onerror = (error)=> { console.log(`WebSocket ${id}: Ошибка соединения:`, error); setWsState('err'); }; ws.onmessage = (ev)=>{ // Устанавливаем состояние 'on' при получении сообщений setWsState('on'); const parts = (ev.data||'').split(/\r?\n/); for (let i=0;i{ const list = state.services.map(s=> `${s.service}`).filter((v,i,a)=> a.indexOf(v)===i).join(', '); const ans = prompt('Введите имена сервисов через запятую:\n'+list); if (ans){ const services = ans.split(',').map(x=>x.trim()).filter(Boolean); if (services.length) openFanGroup(services); } }; } // Функция для обновления счетчиков через Ajax async function updateCounters(containerId) { try { const token = localStorage.getItem('access_token'); if (!token) { console.error('No access token found'); return; } const response = await fetch(`/api/logs/stats/${containerId}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const stats = await response.json(); const cdbg = document.querySelector('.cdbg'); const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); const cother = document.querySelector('.cother'); if (cdbg) cdbg.textContent = stats.debug || 0; if (cinfo) cinfo.textContent = stats.info || 0; if (cwarn) cwarn.textContent = stats.warn || 0; if (cerr) cerr.textContent = stats.error || 0; if (cother) cother.textContent = stats.other || 0; // Обновляем видимость счетчиков updateCounterVisibility(); // Добавляем обработчики для счетчиков 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); } } // Функция для обновления счетчиков в multi view (суммирует статистику всех контейнеров) // Эта функция теперь использует пересчет на основе отображаемых логов async function updateMultiViewCounters() { if (!state.multiViewMode || state.selectedContainers.length === 0) { return; } try { console.log('Updating multi-view counters for containers:', state.selectedContainers); // Используем новую функцию пересчета счетчиков recalculateMultiViewCounters(); // Добавляем обработчики для счетчиков addCounterClickHandlers(); } catch (error) { console.error('Error updating multi-view counters:', error); } } // Функция для пересчета счетчиков на основе отображаемых логов (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, other: 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++; if (logEntry.cls === 'other') obj.counters.other++; } }); // Обновляем отображение счетчиков const cdbg = document.querySelector('.cdbg'); const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); const cother = document.querySelector('.cother'); 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; if (cother) cother.textContent = obj.counters.other; // Обновляем счетчики в кнопках заголовка single-view updateHeaderCounters(containerId, obj.counters); 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; let totalOther = 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, other: 0}; // Пересчитываем счетчики только для отображаемых логов visibleLogs.forEach(logEntry => { const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && 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++; if (logEntry.cls === 'other') obj.counters.other++; } }); // Обновляем счетчики в кнопках заголовка для этого контейнера updateHeaderCounters(containerId, obj.counters); // Добавляем к общим счетчикам totalDebug += obj.counters.dbg; totalInfo += obj.counters.info; totalWarn += obj.counters.warn; totalError += obj.counters.err; totalOther += obj.counters.other; } // Обновляем отображение счетчиков const cdbg = document.querySelector('.cdbg'); const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); const cother = document.querySelector('.cother'); if (cdbg) cdbg.textContent = totalDebug; if (cinfo) cinfo.textContent = totalInfo; if (cwarn) cwarn.textContent = totalWarn; if (cerr) cerr.textContent = totalError; if (cother) cother.textContent = totalOther; console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError, totalOther }); } // Функция для обновления видимости счетчиков function updateCounterVisibility() { // Обновляем старые кнопки счетчиков (только для legacy интерфейса) const debugBtn = document.querySelector('.debug-btn'); const infoBtn = document.querySelector('.info-btn'); const warnBtn = document.querySelector('.warn-btn'); const errorBtn = document.querySelector('.error-btn'); const otherBtn = document.querySelector('.other-btn'); if (debugBtn) { debugBtn.classList.toggle('disabled', !state.levels.debug); } if (infoBtn) { infoBtn.classList.toggle('disabled', !state.levels.info); } if (warnBtn) { warnBtn.classList.toggle('disabled', !state.levels.warn); } if (errorBtn) { errorBtn.classList.toggle('disabled', !state.levels.err); } if (otherBtn) { otherBtn.classList.toggle('disabled', !state.levels.other); } } // Функция для управления видимостью кнопок LogLevels function updateLogLevelsVisibility() { const singleViewLevels = document.querySelector('.single-view-levels'); const multiViewLevels = document.querySelectorAll('.multi-view-levels'); // Проверяем, есть ли выбранные контейнеры const hasSelectedContainers = state.selectedContainers.length > 0 || state.current; // Управляем видимостью кнопок в single view if (singleViewLevels) { if (hasSelectedContainers) { singleViewLevels.style.display = 'flex'; } else { singleViewLevels.style.display = 'none'; } } // Управляем видимостью кнопок в multi view multiViewLevels.forEach(levelsContainer => { if (hasSelectedContainers) { levelsContainer.style.display = 'flex'; } else { levelsContainer.style.display = 'none'; } }); } // Функция для обновления логов и счетчиков async function refreshLogsAndCounters() { if (state.multiViewMode && state.selectedContainers.length > 0) { // Обновляем мультипросмотр console.log('Refreshing multi-view for containers:', state.selectedContainers); // Очищаем логи в мультипросмотре перед обновлением state.selectedContainers.forEach(containerId => { const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (multiViewLog) { multiViewLog.textContent = 'Refreshing...'; } // Очищаем буфер логов для мультипросмотра const obj = state.open[containerId]; if (obj && obj.allLogs) { obj.allLogs = []; } }); // Перезапускаем WebSocket соединения для всех выбранных контейнеров state.selectedContainers.forEach(containerId => { closeWs(containerId); const service = state.services.find(s => s.id === containerId); if (service) { openMultiViewWs(service); } }); // Пересчитываем счетчики на основе отображаемых логов setTimeout(() => { recalculateMultiViewCounters(); // Применяем настройки wrap text после обновления applyWrapSettings(); }, 1000); // Небольшая задержка для завершения переподключения } else if (state.current) { // Обычный режим просмотра console.log('Refreshing logs and counters for:', state.current.id); // Очищаем логи перед обновлением if (els.logContent) { els.logContent.textContent = 'Refreshing...'; } // Перезапускаем WebSocket соединение для получения свежих логов const currentId = state.current.id; closeWs(currentId); // Находим обновленный контейнер в списке const updatedContainer = state.services.find(s => s.id === currentId); if (updatedContainer) { // Переключаемся на обновленный контейнер await switchToSingle(updatedContainer); // Очищаем лишние пустые строки после переключения if (els.logContent) { cleanSingleViewEmptyLines(els.logContent); cleanDuplicateLines(els.logContent); } } // Пересчитываем счетчики на основе отображаемых логов setTimeout(() => { recalculateCounters(); // Применяем настройки wrap text после обновления applyWrapSettings(); }, 1000); // Небольшая задержка для завершения переподключения } else { console.log('No container selected'); } } // 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 = ''; // Очищаем лишние пустые строки после очистки 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) { multiViewLog.textContent = ''; } // Очищаем буфер логов для мультипросмотра const obj = state.open[containerId]; if (obj && obj.allLogs) { obj.allLogs = []; } }); } // Сбрасываем счетчики 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 () => { console.log('Refreshing services...'); await fetchServices(); if (state.multiViewMode && state.selectedContainers.length > 0) { // В Multi View режиме обновляем все выбранные контейнеры console.log('Refreshing Multi View mode with containers:', state.selectedContainers); // Закрываем все текущие соединения state.selectedContainers.forEach(containerId => { closeWs(containerId); }); // Перезапускаем соединения для всех выбранных контейнеров state.selectedContainers.forEach(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('Reconnecting to current container:', state.current.id); const currentId = state.current.id; // Закрываем текущее соединение closeWs(currentId); // Находим обновленный контейнер в списке const updatedContainer = state.services.find(s => s.id === currentId); if (updatedContainer) { // Переключаемся на обновленный контейнер await switchToSingle(updatedContainer); } else { // Если контейнер больше не существует, не открываем автоматически первый доступный // Пользователь сам выберет нужный контейнер console.log('Container no longer exists, not auto-opening first available container'); } } }; // Обработчик для кнопок refresh логов (в log-header и в header) document.querySelectorAll('.log-refresh-btn').forEach(btn=>{ btn.addEventListener('click', refreshLogsAndCounters); }); // Обработчик для кнопки update (AJAX autoupdate toggle) if (els.ajaxUpdateBtn) { console.log('Инициализация обработчика клика для кнопки update'); els.ajaxUpdateBtn.addEventListener('click', () => { console.log('Клик по кнопке update - вызываем toggleAjaxLogUpdate()'); toggleAjaxLogUpdate(); }); } else { console.error('Кнопка ajaxUpdateBtn не найдена при инициализации обработчика!'); } // Обработчики для счетчиков function addCounterClickHandlers() { // Назначаем обработчики на все дубликаты кнопок (и в log-header, и в header-compact-controls) const debugButtons = document.querySelectorAll('.debug-btn'); const infoButtons = document.querySelectorAll('.info-btn'); const warnButtons = document.querySelectorAll('.warn-btn'); const errorButtons = document.querySelectorAll('.error-btn'); const otherButtons = document.querySelectorAll('.other-btn'); debugButtons.forEach(debugBtn => debugBtn.onclick = () => { state.levels.debug = !state.levels.debug; updateCounterVisibility(); refreshAllLogs(); // Добавляем refresh для обновления логов if (state.current) { refreshLogsAndCounters(); } // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); } }); infoButtons.forEach(infoBtn => infoBtn.onclick = () => { state.levels.info = !state.levels.info; updateCounterVisibility(); refreshAllLogs(); // Добавляем refresh для обновления логов if (state.current) { refreshLogsAndCounters(); } // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); } }); warnButtons.forEach(warnBtn => warnBtn.onclick = () => { state.levels.warn = !state.levels.warn; updateCounterVisibility(); refreshAllLogs(); // Добавляем refresh для обновления логов if (state.current) { refreshLogsAndCounters(); } // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); } }); errorButtons.forEach(errorBtn => errorBtn.onclick = () => { state.levels.err = !state.levels.err; updateCounterVisibility(); refreshAllLogs(); // Добавляем refresh для обновления логов if (state.current) { refreshLogsAndCounters(); } // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); } }); otherButtons.forEach(otherBtn => otherBtn.onclick = () => { state.levels.other = !state.levels.other; updateCounterVisibility(); refreshAllLogs(); // Добавляем refresh для обновления логов if (state.current) { refreshLogsAndCounters(); } // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); } }); } // Функция для добавления обработчиков мультивыбора проектов function addMultiSelectHandlers() { const display = document.getElementById('projectSelectDisplay'); const dropdown = document.getElementById('projectSelectDropdown'); console.log('Adding multi-select handlers, elements found:', {display: !!display, dropdown: !!dropdown}); if (display && dropdown) { // Обработчик клика по дисплею display.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = dropdown.style.display !== 'none'; if (isOpen) { dropdown.style.display = 'none'; display.classList.remove('active'); } else { dropdown.style.display = 'block'; display.classList.add('active'); } }); // Обработчики кликов по опциям dropdown.addEventListener('click', async (e) => { e.stopPropagation(); const option = e.target.closest('.multi-select-option'); if (!option) return; const checkbox = option.querySelector('input[type="checkbox"]'); if (!checkbox) return; const value = option.getAttribute('data-value'); // Специальная логика для "All Projects" if (value === 'all') { if (checkbox.checked) { // Если "All Projects" выбран, снимаем все остальные dropdown.querySelectorAll('.multi-select-option input[type="checkbox"]').forEach(cb => { if (cb !== checkbox) cb.checked = false; }); } } else { // Если выбран конкретный проект, снимаем "All Projects" const allCheckbox = dropdown.querySelector('#project-all'); if (allCheckbox) allCheckbox.checked = false; } // Обновляем отображение и загружаем сервисы const selectedProjects = getSelectedProjects(); updateMultiSelect(selectedProjects); await fetchServices(); }); // Закрытие dropdown при клике вне его document.addEventListener('click', (e) => { if (!display.contains(e.target) && !dropdown.contains(e.target)) { dropdown.style.display = 'none'; display.classList.remove('active'); } }); } } // Функция для показа уведомления о горячих клавишах function showHotkeysNotification() { // Создаем уведомление const notification = document.createElement('div'); notification.className = 'hotkeys-notification'; notification.innerHTML = `

Горячие клавиши

  • [ ] - Навигация между контейнерами
  • Ctrl + R или Ctrl + K - Обновить логи
  • Ctrl + B - Свернуть/развернуть панель
  • Кнопка - управление панелью
`; document.body.appendChild(notification); // Автоматически скрываем через 8 секунд setTimeout(() => { if (notification.parentElement) { notification.remove(); } }, 8000); } // Функция для сворачивания/разворачивания sidebar и header function toggleSidebar() { if (els.sidebar) { const isCollapsed = els.sidebar.classList.contains('collapsed'); if (isCollapsed) { // Разворачиваем sidebar els.sidebar.classList.remove('collapsed'); els.sidebarToggle.innerHTML = ''; els.sidebarToggle.title = 'Свернуть панель (Ctrl+B / Ctrl+И)'; localStorage.setItem('lb_sidebar_collapsed', 'false'); } else { // Сворачиваем sidebar els.sidebar.classList.add('collapsed'); els.sidebarToggle.innerHTML = ''; els.sidebarToggle.title = 'Развернуть панель (Ctrl+B / Ctrl+И)'; localStorage.setItem('lb_sidebar_collapsed', 'true'); } // Принудительно обновляем стили логов после переключения sidebar setTimeout(() => { updateLogStyles(); // Дополнительная проверка для multi-view логов if (state.multiViewMode) { console.log('Sidebar toggle: Force fixing multi-view styles'); forceFixMultiViewStyles(); } }, 100); } } // Функция для принудительного исправления стилей multi-view логов function forceFixMultiViewStyles() { const multiViewLogs = document.querySelectorAll('.multi-view-content .multi-view-log'); console.log(`Force fixing styles for ${multiViewLogs.length} multi-view logs`); multiViewLogs.forEach((log, index) => { const containerId = log.getAttribute('data-container-id'); console.log(`Force fixing multi-view log ${index + 1} for container: ${containerId}`); // Универсальное исправление для всех контейнеров log.style.setProperty('height', '100%', 'important'); log.style.setProperty('overflow', 'auto', 'important'); log.style.setProperty('max-height', 'none', 'important'); log.style.setProperty('display', 'block', 'important'); log.style.setProperty('min-height', '200px', 'important'); log.style.setProperty('position', 'relative', 'important'); log.style.setProperty('flex', '1', 'important'); log.style.setProperty('min-height', '0', 'important'); log.style.setProperty('width', '100%', 'important'); log.style.setProperty('box-sizing', 'border-box', 'important'); // Принудительно вызываем пересчет layout log.style.setProperty('transform', 'translateZ(0)', 'important'); // Устанавливаем универсальные inline стили для всех контейнеров const currentStyle = log.getAttribute('style') || ''; const newStyle = currentStyle + '; height: 100% !important; overflow: auto !important; flex: 1 !important; min-height: 0 !important; width: 100% !important; box-sizing: border-box !important; display: block !important; position: relative !important;'; log.setAttribute('style', newStyle); // Проверяем и исправляем родительские элементы для всех контейнеров const parentContent = log.closest('.multi-view-content'); if (parentContent) { parentContent.style.setProperty('display', 'flex', 'important'); parentContent.style.setProperty('flex-direction', 'column', 'important'); parentContent.style.setProperty('overflow', 'hidden', 'important'); parentContent.style.setProperty('height', '100%', 'important'); } const parentPanel = log.closest('.multi-view-panel'); if (parentPanel) { parentPanel.style.setProperty('display', 'flex', 'important'); parentPanel.style.setProperty('flex-direction', 'column', 'important'); parentPanel.style.setProperty('overflow', 'hidden', 'important'); parentPanel.style.setProperty('height', '100%', 'important'); } }); // Также исправляем стили для multi-view-content контейнеров const multiViewContents = document.querySelectorAll('.multi-view-content'); multiViewContents.forEach(content => { content.style.setProperty('display', 'flex', 'important'); content.style.setProperty('flex-direction', 'column', 'important'); content.style.setProperty('overflow', 'hidden', 'important'); content.style.setProperty('height', '100%', 'important'); }); // Универсальное исправление для всех контейнеров multiViewLogs.forEach(log => { console.log(`Universal fix for container:`, log.getAttribute('data-container-id')); // Принудительно устанавливаем все стили заново для всех контейнеров log.style.cssText = 'height: 100% !important; overflow: auto !important; max-height: none !important; display: block !important; min-height: 200px !important; position: relative !important; flex: 1 !important; min-height: 0 !important; width: 100% !important; box-sizing: border-box !important;'; // Принудительно вызываем пересчет layout log.style.setProperty('transform', 'translateZ(0)', 'important'); }); // Применяем настройки wrap text после исправления стилей applyWrapSettings(); } // Функция для обновления стилей логов function updateLogStyles() { const isCollapsed = els.sidebar && els.sidebar.classList.contains('collapsed'); // Обновляем стили для single-view логов const singleViewLogs = document.querySelectorAll('.single-view-content .log'); singleViewLogs.forEach(log => { if (isCollapsed) { log.style.height = 'calc(100vh - var(--header-height))'; log.style.overflow = 'auto'; } else { log.style.height = '100%'; log.style.overflow = 'auto'; } }); // Обновляем стили для multi-view логов (более агрессивно) const multiViewLogs = document.querySelectorAll('.multi-view-content .multi-view-log'); console.log(`Found ${multiViewLogs.length} multi-view logs to update`); multiViewLogs.forEach((log, index) => { const containerId = log.getAttribute('data-container-id'); console.log(`Updating multi-view log ${index + 1}/${multiViewLogs.length} for container: ${containerId}`); // Принудительно устанавливаем правильные стили независимо от состояния sidebar log.style.setProperty('height', '100%', 'important'); log.style.setProperty('overflow', 'auto', 'important'); log.style.setProperty('max-height', 'none', 'important'); log.style.setProperty('display', 'block', 'important'); log.style.setProperty('min-height', '200px', 'important'); log.style.setProperty('position', 'relative', 'important'); log.style.setProperty('flex', '1', 'important'); log.style.setProperty('min-height', '0', 'important'); // Принудительно вызываем пересчет layout log.style.setProperty('transform', 'translateZ(0)', 'important'); }); // Также обновляем стили для multi-view-content контейнеров const multiViewContents = document.querySelectorAll('.multi-view-content'); multiViewContents.forEach(content => { if (isCollapsed) { // В свернутом состоянии multi-view-content должен иметь правильную высоту content.style.setProperty('height', 'calc(100vh - var(--header-height) - 60px)', 'important'); } else { content.style.setProperty('height', '100%', 'important'); } content.style.setProperty('overflow', 'hidden', 'important'); content.style.setProperty('display', 'flex', 'important'); content.style.setProperty('flex-direction', 'column', 'important'); }); // Применяем настройки wrap text applyWrapSettings(); console.log('Log styles updated, sidebar collapsed:', isCollapsed, 'multi-view logs found:', multiViewLogs.length); // Принудительно исправляем стили multi-view логов forceFixMultiViewStyles(); // Дополнительная проверка через 500ms для multi view логов if (multiViewLogs.length > 0) { setTimeout(() => { console.log('Performing delayed update for multi-view logs...'); forceFixMultiViewStyles(); }, 500); } } // Mobile menu toggle if (els.mobileToggle) { els.mobileToggle.onclick = () => { const sidebar = document.querySelector('.sidebar'); if (sidebar) { sidebar.classList.toggle('open'); } }; } // Функция для показа/скрытия модального окна с горячими клавишами function toggleHotkeysModal() { if (els.hotkeysModal) { const isVisible = els.hotkeysModal.classList.contains('show'); if (isVisible) { els.hotkeysModal.classList.remove('show'); } else { els.hotkeysModal.classList.add('show'); } } } // Sidebar toggle button if (els.sidebarToggle) { els.sidebarToggle.onclick = toggleSidebar; } // Modal close button if (els.hotkeysModalClose) { els.hotkeysModalClose.onclick = toggleHotkeysModal; } // Close modal on background click if (els.hotkeysModal) { els.hotkeysModal.onclick = (e) => { if (e.target === els.hotkeysModal) { toggleHotkeysModal(); } }; } // Collapsible sections document.addEventListener('DOMContentLoaded', () => { // Обработчики для сворачивания секций document.querySelectorAll('.control-header').forEach(header => { header.addEventListener('click', (e) => { if (e.target.closest('.collapse-btn')) return; // Не сворачиваем при клике на кнопку const group = header.closest('.control-group'); // Если секция минимизирована, сначала разворачиваем if (group.classList.contains('minimized')) { group.classList.remove('minimized'); group.classList.add('collapsed'); const section = group.dataset.section; localStorage.setItem(`lb_minimized_${section}`, 'false'); localStorage.setItem(`lb_collapsed_${section}`, 'true'); } else { // Обычное сворачивание/разворачивание group.classList.toggle('collapsed'); const section = group.dataset.section; localStorage.setItem(`lb_collapsed_${section}`, group.classList.contains('collapsed')); localStorage.setItem(`lb_minimized_${section}`, 'false'); } }); }); // Обработчики для кнопок сворачивания document.querySelectorAll('.collapse-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const group = btn.closest('.control-group'); // Если секция минимизирована, сначала разворачиваем if (group.classList.contains('minimized')) { group.classList.remove('minimized'); group.classList.add('collapsed'); const section = group.dataset.section; localStorage.setItem(`lb_minimized_${section}`, 'false'); localStorage.setItem(`lb_collapsed_${section}`, 'true'); } else { // Обычное сворачивание/разворачивание group.classList.toggle('collapsed'); const section = group.dataset.section; localStorage.setItem(`lb_collapsed_${section}`, group.classList.contains('collapsed')); localStorage.setItem(`lb_minimized_${section}`, 'false'); } }); }); // Восстанавливаем состояние секций из localStorage или сворачиваем по умолчанию document.querySelectorAll('.control-group.collapsible').forEach(group => { const section = group.dataset.section; const savedCollapsed = localStorage.getItem(`lb_collapsed_${section}`); const savedMinimized = localStorage.getItem(`lb_minimized_${section}`); // Если состояние не сохранено, сворачиваем по умолчанию if (savedCollapsed === null && savedMinimized === null) { group.classList.add('collapsed'); localStorage.setItem(`lb_collapsed_${section}`, 'true'); localStorage.setItem(`lb_minimized_${section}`, 'false'); } else if (savedMinimized === 'true') { group.classList.add('minimized'); group.classList.remove('collapsed'); } else if (savedCollapsed === 'true') { group.classList.add('collapsed'); group.classList.remove('minimized'); } }); // Обработчик для кнопки Options if (els.optionsBtn) { els.optionsBtn.addEventListener('click', () => { const sidebarControls = document.querySelector('.sidebar-controls'); const isHidden = sidebarControls.classList.contains('hidden'); if (isHidden) { // Если сайдбар свернут, сначала разворачиваем его if (els.sidebar.classList.contains('collapsed')) { toggleSidebar(); } // Показываем настройки sidebarControls.classList.remove('hidden'); els.optionsBtn.classList.remove('active'); els.optionsBtn.title = 'Скрыть настройки'; localStorage.setItem('lb_options_hidden', 'false'); } else { // Скрываем настройки sidebarControls.classList.add('hidden'); els.optionsBtn.classList.add('active'); els.optionsBtn.title = 'Показать настройки'; localStorage.setItem('lb_options_hidden', 'true'); } }); // Восстанавливаем состояние кнопки Options (по умолчанию скрыто) const optionsHidden = localStorage.getItem('lb_options_hidden'); if (optionsHidden === null || optionsHidden === 'true') { document.querySelector('.sidebar-controls').classList.add('hidden'); els.optionsBtn.classList.add('active'); els.optionsBtn.title = 'Показать настройки'; localStorage.setItem('lb_options_hidden', 'true'); } // Инициализируем состояние кнопок уровней логирования initializeLevelButtons(); } // Обработчик для кнопки выхода if (els.logoutBtn) { els.logoutBtn.addEventListener('click', async () => { if (confirm('Вы уверены, что хотите выйти?')) { try { // Вызываем API для выхода await fetch('/api/auth/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` } }); } catch (error) { console.error('Logout error:', error); } finally { // Останавливаем автоматическую проверку WebSocket stopWebSocketStatusCheck(); // Очищаем localStorage localStorage.removeItem('access_token'); // Перенаправляем на страницу входа window.location.href = '/login'; } } }); } // Инициализируем стили логов при загрузке страницы updateLogStyles(); // Применяем настройки wrap text при загрузке applyWrapSettings(); // Дополнительная проверка для multi-view логов при загрузке setTimeout(() => { if (state.multiViewMode) { console.log('Initialization: Force fixing multi-view styles'); forceFixMultiViewStyles(); } }, 1000); // Обработчик для кнопки помощи if (els.helpBtn) { const helpTooltip = document.getElementById('helpTooltip'); let tooltipTimeout; // Показ модального окна при клике els.helpBtn.addEventListener('click', () => { showHelpTooltip(); }); // Кнопка закрытия модального окна const helpTooltipClose = document.getElementById('helpTooltipClose'); if (helpTooltipClose) { helpTooltipClose.addEventListener('click', () => { hideHelpTooltip(); }); } // Закрытие по Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { hideHelpTooltip(); } }); // Закрытие по клику вне модального окна helpTooltip.addEventListener('click', (e) => { if (e.target === helpTooltip) { hideHelpTooltip(); } }); } }); if (els.snapshotBtn) { els.snapshotBtn.onclick = ()=>{ if (state.multiViewMode && state.selectedContainers.length > 0) { // В Multi View режиме используем первый выбранный контейнер как ID для sendSnapshot // Функция sendSnapshot сама определит, что нужно скачать логи всех контейнеров sendSnapshot(state.selectedContainers[0]); } else if (state.current) { sendSnapshot(state.current.id); } else { alert('No container selected'); } }; } if (els.tail) { els.tail.onchange = ()=> { Object.keys(state.open).forEach(id=>{ const svc = state.services.find(s=> s.id===id); if (!svc) return; // В multi view режиме используем openMultiViewWs if (state.multiViewMode && state.selectedContainers.includes(id)) { console.log(`Refresh: Using openMultiViewWs for ${svc.name} in multi view mode`); closeWs(id); openMultiViewWs(svc); } else { // В обычном режиме используем openWs const panel = els.grid.querySelector(`.panel[data-cid="${id}"]`); if (!panel) return; state.open[id].logEl.textContent=''; closeWs(id); openWs(svc, panel); } }); // Обновляем современный интерфейс if (state.current && els.logContent) { els.logContent.textContent = 'Reconnecting...'; } // Пересчитываем счетчики после изменения Tail Lines setTimeout(() => { if (state.multiViewMode) { recalculateMultiViewCounters(); } else { recalculateCounters(); } }, 1000); // Небольшая задержка для завершения переподключения }; } if (els.wrapToggle) { els.wrapToggle.onchange = ()=> { applyWrapSettings(); }; } // Добавляем обработчики для autoscroll и pause if (els.autoscroll) { els.autoscroll.onchange = ()=> { // Обновляем настройку автопрокрутки для всех открытых логов Object.keys(state.open).forEach(id => { const obj = state.open[id]; if (obj && obj.wrapEl) { if (els.autoscroll.checked) { obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; } } }); // Обновляем современный интерфейс if (state.current && els.logContent) { const logContent = document.querySelector('.log-content'); if (logContent && els.autoscroll.checked) { 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; } }); } }; } // Обработчик для фильтра (если элемент существует) if (els.filter) { els.filter.oninput = ()=> { state.filter = els.filter.value.trim(); refreshAllLogs(); // Пересчитываем счетчики в зависимости от режима setTimeout(() => { if (state.multiViewMode) { recalculateMultiViewCounters(); } else { recalculateCounters(); } }, 100); }; } // Обработчики для LogLevels (если элементы существуют) if (els.lvlDebug) { els.lvlDebug.onchange = ()=> { state.levels.debug = els.lvlDebug.checked; updateCounterVisibility(); refreshAllLogs(); // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); setTimeout(() => { recalculateMultiViewCounters(); }, 100); } else { // Пересчитываем счетчики для Single View setTimeout(() => { recalculateCounters(); }, 100); } }; } if (els.lvlInfo) { els.lvlInfo.onchange = ()=> { state.levels.info = els.lvlInfo.checked; updateCounterVisibility(); refreshAllLogs(); // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); setTimeout(() => { recalculateMultiViewCounters(); }, 100); } else { // Пересчитываем счетчики для Single View setTimeout(() => { recalculateCounters(); }, 100); } }; } if (els.lvlWarn) { els.lvlWarn.onchange = ()=> { state.levels.warn = els.lvlWarn.checked; updateCounterVisibility(); refreshAllLogs(); // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); setTimeout(() => { recalculateMultiViewCounters(); }, 100); } else { // Пересчитываем счетчики для Single View setTimeout(() => { recalculateCounters(); }, 100); } }; } if (els.lvlErr) { els.lvlErr.onchange = ()=> { state.levels.err = els.lvlErr.checked; updateCounterVisibility(); refreshAllLogs(); // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); setTimeout(() => { recalculateMultiViewCounters(); }, 100); } else { // Пересчитываем счетчики для Single View setTimeout(() => { recalculateCounters(); }, 100); } }; } if (els.lvlOther) { els.lvlOther.onchange = ()=> { state.levels.other = els.lvlOther.checked; updateCounterVisibility(); refreshAllLogs(); // Обновляем multi-view если он активен if (state.multiViewMode) { refreshAllLogs(); setTimeout(() => { recalculateMultiViewCounters(); }, 100); } else { // Пересчитываем счетчики для Single View setTimeout(() => { recalculateCounters(); }, 100); } }; } // Обработчик изменения размера окна для обновления стилей multi-view логов window.addEventListener('resize', () => { if (state.multiViewMode) { console.log('Window resize: Force fixing multi-view styles'); setTimeout(() => { forceFixMultiViewStyles(); // Дополнительно исправляем все контейнеры console.log('Window resize: Fixing all containers'); if (window.fixAllContainers) { window.fixAllContainers(); } }, 100); } }); // Hotkeys: [ ] х ъ — navigation between containers, Ctrl/Cmd+R/K — refresh logs, Ctrl/Cmd+B/И — toggle sidebar window.addEventListener('keydown', async (e)=>{ // Проверяем, не находится ли фокус в поле ввода const activeElement = document.activeElement; const isInputActive = activeElement && ( activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.contentEditable === 'true' ); // Если фокус в поле ввода, не обрабатываем горячие клавиши if (isInputActive) { return; } // Навигация между контейнерами по [ ] х ъ if (e.key==='[' || e.key==='х'){ e.preventDefault(); const idx = state.services.findIndex(s=> s.id===state.current?.id); if (idx>0) await switchToSingle(state.services[idx-1]); } if (e.key===']' || e.key==='ъ'){ e.preventDefault(); const idx = state.services.findIndex(s=> s.id===state.current?.id); if (idx>=0 && idx { // Сначала попробуем использовать els.filter let filterElement = els.filter; // Если els.filter не найден, попробуем найти элемент напрямую if (!filterElement) { console.log('els.filter not found, searching directly...'); filterElement = document.getElementById('filter'); } // Если элемент найден, фокусируемся на нем if (filterElement) { console.log('Focusing on filter element:', filterElement); try { filterElement.focus(); filterElement.select(); console.log('Filter focused successfully'); } catch (error) { console.error('Error focusing filter:', error); } } else { console.error('Filter element not found anywhere!'); // Попробуем еще раз через небольшую задержку setTimeout(() => { const retryElement = document.getElementById('filter'); if (retryElement) { console.log('Filter found on retry, focusing...'); retryElement.focus(); retryElement.select(); } }, 100); } }; // Вызываем функцию фокусировки focusFilter(); } }); // Функция для переинициализации элементов function reinitializeElements() { // Переинициализируем элементы, которые могут быть не найдены при первой загрузке els.filter = document.getElementById('filter'); els.containerList = document.getElementById('containerList'); els.logContent = document.getElementById('logContent'); els.mobileToggle = document.getElementById('mobileToggle'); els.optionsBtn = document.getElementById('optionsBtn'); els.helpBtn = document.getElementById('helpBtn'); els.logoutBtn = document.getElementById('logoutBtn'); els.sidebar = document.getElementById('sidebar'); els.sidebarToggle = document.getElementById('sidebarToggle'); els.header = document.getElementById('header'); console.log('Elements reinitialized:', { filter: !!els.filter, containerList: !!els.containerList, logContent: !!els.logContent, sidebar: !!els.sidebar, sidebarToggle: !!els.sidebarToggle }); } // Инициализация (async function init() { console.log('Initializing LogBoard+...'); // Переинициализируем элементы reinitializeElements(); // Инициализируем состояние WebSocket setWsState('off'); // Дополнительно инициализируем элементы после полной загрузки DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', reinitializeElements); } // Инициализируем элементы после полной загрузки страницы window.addEventListener('load', reinitializeElements); // Обработчик для правильной очистки при перезагрузке страницы window.addEventListener('beforeunload', () => { // Останавливаем автоматическую проверку WebSocket stopWebSocketStatusCheck(); // Закрываем все WebSocket соединения Object.keys(state.open).forEach(id => { const obj = state.open[id]; if (obj && obj.ws) { try { obj.ws.close(); } catch (e) { // Игнорируем ошибки при закрытии } } }); // Очищаем состояние state.open = {}; }); // Проверяем авторизацию const token = localStorage.getItem('access_token'); if (!token) { console.log('No access token found, redirecting to login'); window.location.href = '/login'; return; } // Проверяем валидность токена try { const response = await fetch('/api/auth/me', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { console.log('Invalid token, redirecting to login'); localStorage.removeItem('access_token'); window.location.href = '/login'; return; } } catch (error) { console.error('Error checking auth:', error); localStorage.removeItem('access_token'); window.location.href = '/login'; return; } console.log('Elements found:', { containerList: !!els.containerList, logContent: !!els.logContent, mobileToggle: !!els.mobileToggle, themeSwitch: !!els.themeSwitch }); // Проверяем header project select const headerSelect = document.getElementById('projectSelectHeader'); console.log('Header project select found during init:', !!headerSelect); await fetchProjects(); await fetchServices(); // Проверяем состояние WebSocket после загрузки сервисов setTimeout(() => { console.log('Проверка состояния WebSocket после загрузки сервисов'); setWsState(determineWsState()); }, 1000); // Запускаем автоматическую проверку состояния WebSocket startWebSocketStatusCheck(); // Добавляем обработчик клика для кнопки WebSocket статуса if (els.wsstate) { els.wsstate.addEventListener('click', () => { console.log('Ручная проверка состояния WebSocket'); checkWebSocketStatus(); }); } // Проверяем, есть ли сохраненный контейнер в localStorage const savedContainerId = getSelectedContainerFromStorage(); if (savedContainerId) { console.log('Found saved container, switching to it:', savedContainerId); const savedService = state.services.find(s => s.id === savedContainerId); if (savedService) { // Добавляем контейнер в выбранные state.selectedContainers = [savedContainerId]; // Переключаемся на сохраненный контейнер await switchToSingle(savedService); // Очищаем сохраненный контейнер из localStorage saveSelectedContainer(null); } else { console.log('Saved container not found in services, clearing localStorage'); saveSelectedContainer(null); } } // Инициализируем видимость счетчиков updateCounterVisibility(); // Обновляем состояние чекбоксов после загрузки сервисов updateContainerSelectionUI(); // Восстанавливаем состояние sidebar const sidebarCollapsed = localStorage.getItem('lb_sidebar_collapsed'); if (sidebarCollapsed === 'true' && els.sidebar && els.sidebarToggle) { els.sidebar.classList.add('collapsed'); els.sidebarToggle.innerHTML = ''; els.sidebarToggle.title = 'Развернуть панель (Ctrl+B / Ctrl+И)'; } // Показываем подсказку о горячих клавишах при первом запуске const hotkeysShown = localStorage.getItem('lb_hotkeys_shown'); if (!hotkeysShown) { setTimeout(() => { showHotkeysNotification(); localStorage.setItem('lb_hotkeys_shown', 'true'); }, 2000); } // Добавляем обработчики для счетчиков addCounterClickHandlers(); // Добавляем обработчик для выпадающего списка проектов в заголовке addMultiSelectHandlers(); // Загружаем и отображаем исключенные контейнеры loadExcludedContainers().then(containers => { renderExcludedContainers(containers); }); // Добавляем обработчики для исключенных контейнеров const addExcludedBtn = document.getElementById('addExcludedContainer'); const newExcludedInput = document.getElementById('newExcludedContainer'); if (addExcludedBtn) { addExcludedBtn.onclick = addExcludedContainer; } if (newExcludedInput) { newExcludedInput.onkeypress = (e) => { if (e.key === 'Enter') { addExcludedContainer(); } }; } // Добавляем обработчики для чекбоксов контейнеров document.addEventListener('change', (e) => { if (e.target.classList.contains('container-checkbox')) { const containerId = e.target.getAttribute('data-container-id'); toggleContainerSelection(containerId); } // Обработчик изменения tail lines if (e.target.id === 'tail') { console.log('Tail lines changed to:', e.target.value); if (state.multiViewMode) { // В multi view применяем новое ограничение к уже отображаемым логам const tailLines = parseInt(e.target.value) || 50; console.log(`Applying tail lines limit ${tailLines} to ${state.selectedContainers.length} containers:`, state.selectedContainers); // Проверяем все элементы multi-view-log на странице const allMultiViewLogs = document.querySelectorAll('.multi-view-log'); console.log(`Found ${allMultiViewLogs.length} multi-view-log elements on page:`, Array.from(allMultiViewLogs).map(el => el.getAttribute('data-container-id'))); state.selectedContainers.forEach(containerId => { console.log(`Processing container ${containerId}...`); // Ищем элемент несколькими способами let multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (!multiViewLog) { console.warn(`Container ${containerId} not found with data-container-id, trying alternative search...`); // Попробуем найти по другому селектору multiViewLog = document.querySelector(`[data-container-id="${containerId}"]`); } if (multiViewLog) { console.log(`Found multi-view-log for container ${containerId}, current lines:`, multiViewLog.querySelectorAll('.line').length); // Получаем все строки логов const logLines = Array.from(multiViewLog.querySelectorAll('.line')); console.log(`Container ${containerId}: ${logLines.length} log lines found`); if (logLines.length > tailLines) { // Удаляем лишние строки с начала const linesToRemove = logLines.length - tailLines; console.log(`Removing ${linesToRemove} lines from container ${containerId}`); // Удаляем первые N строк logLines.slice(0, linesToRemove).forEach(line => { line.remove(); }); const remainingLines = multiViewLog.querySelectorAll('.line').length; console.log(`Container ${containerId} now has ${remainingLines} lines after trimming`); } else { console.log(`Container ${containerId} has ${logLines.length} lines, no trimming needed (limit: ${tailLines})`); } } else { console.error(`Multi-view log element not found for container ${containerId}`); console.error(`Available multi-view-log elements:`, Array.from(document.querySelectorAll('.multi-view-log')).map(el => ({ containerId: el.getAttribute('data-container-id'), className: el.className, parent: el.parentElement?.className }))); } }); } } }); // Добавляем обработчики кликов на label чекбоксов document.addEventListener('click', (e) => { if (e.target.classList.contains('container-checkbox-label')) { e.preventDefault(); e.stopPropagation(); const label = e.target; const checkbox = label.previousElementSibling; if (checkbox && checkbox.classList.contains('container-checkbox')) { checkbox.checked = !checkbox.checked; const containerId = checkbox.getAttribute('data-container-id'); toggleContainerSelection(containerId); } } }); // Обработчики для кнопок уровней логирования в заголовках document.addEventListener('click', (e) => { if (e.target.closest('.level-btn')) { const levelBtn = e.target.closest('.level-btn'); const level = levelBtn.getAttribute('data-level'); const containerId = levelBtn.getAttribute('data-container-id'); console.log(`Клик по кнопке уровня логирования: level=${level}, containerId=${containerId}, multiViewMode=${state.multiViewMode}`); // Переключаем состояние кнопки const isActive = levelBtn.classList.contains('active'); levelBtn.classList.toggle('active'); // Обновляем состояние уровней логирования if (containerId) { // Для multi-view: конкретный контейнер if (!state.containerLevels) { state.containerLevels = {}; } if (!state.containerLevels[containerId]) { state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true}; } state.containerLevels[containerId][level] = !isActive; // Сохраняем состояние кнопок loglevels в localStorage saveLogLevelsState(); // Обновляем видимость логов только для этого контейнера updateContainerLogVisibility(containerId); // Пересчитываем счетчики только для этого контейнера setTimeout(() => { updateContainerCounters(containerId); }, 100); // Обновляем видимость логов для всех контейнеров в multi-view // чтобы убедиться, что изменения применились только к нужному контейнеру state.selectedContainers.forEach(id => { if (id !== containerId) { updateContainerLogVisibility(id); } }); } else { // Для single-view: глобальные настройки state.levels[level] = !isActive; // Сохраняем состояние кнопок loglevels в localStorage saveLogLevelsState(); // Обновляем видимость логов только для текущего контейнера if (state.current) { updateLogVisibility(els.logContent); } // Пересчитываем счетчики только для текущего контейнера setTimeout(() => { recalculateCounters(); }, 100); } } }); // Добавляем тестовые функции в глобальную область для отладки 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; // Добавляем функции для исправления стилей в глобальную область window.forceFixMultiViewStyles = forceFixMultiViewStyles; window.updateLogStyles = updateLogStyles; // Универсальная функция для исправления всех контейнеров window.fixAllContainers = function() { console.log('Fixing all multi-view containers'); const allLogs = document.querySelectorAll('.multi-view-content .multi-view-log'); allLogs.forEach(log => { const containerId = log.getAttribute('data-container-id'); console.log(`Fixing container:`, containerId); // Принудительно устанавливаем все стили заново log.style.cssText = 'height: 100% !important; overflow: auto !important; max-height: none !important; display: block !important; min-height: 200px !important; position: relative !important; flex: 1 !important; min-height: 0 !important; width: 100% !important; box-sizing: border-box !important;'; // Принудительно вызываем пересчет layout log.style.setProperty('transform', 'translateZ(0)', 'important'); // Проверяем родительские элементы const parentContent = log.closest('.multi-view-content'); if (parentContent) { parentContent.style.cssText = 'display: flex !important; flex-direction: column !important; overflow: hidden !important; height: 100% !important;'; } const parentPanel = log.closest('.multi-view-panel'); if (parentPanel) { parentPanel.style.cssText = 'display: flex !important; flex-direction: column !important; overflow: hidden !important; height: 100% !important;'; } }); // Применяем настройки wrap text после исправления всех контейнеров applyWrapSettings(); }; // Оставляем старую функцию для обратной совместимости window.fixProblematicContainers = function() { console.log('fixProblematicContainers is deprecated, use fixAllContainers instead'); window.fixAllContainers(); }; console.log('LogBoard+ инициализирован с исправлениями дублирования строк и правильными переносами строк в Single View и MultiView режимах'); console.log('Для тестирования используйте: testDuplicateRemoval(), testSingleViewDuplicateRemoval(), testSingleViewEmptyLinesRemoval() или testSingleViewLineBreaks()'); // Запускаем первоначальную очистку пустых строк setTimeout(() => { if (!state.multiViewMode && els.logContent) { cleanSingleViewEmptyLines(els.logContent); cleanDuplicateLines(els.logContent); } }, 1000); // Инициализируем видимость кнопок LogLevels updateLogLevelsVisibility(); // ======================================== // AJAX ОБНОВЛЕНИЕ ЛОГОВ // ======================================== // Глобальные переменные для AJAX обновления let ajaxUpdateInterval = null; let ajaxUpdateEnabled = true; // По умолчанию включен let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env) // Состояние для каждого контейнера (для multi-view) let containerStates = new Map(); // containerId -> {lastTimestamp, lastSecondCount} /** * Включить периодическое обновление логов через AJAX * @param {number} intervalMs - Интервал обновления в миллисекундах */ function enableAjaxLogUpdate(intervalMs = null) { if (ajaxUpdateInterval) { clearInterval(ajaxUpdateInterval); } // Используем переданный интервал или значение по умолчанию if (intervalMs === null) { intervalMs = ajaxUpdateIntervalMs; } ajaxUpdateEnabled = true; ajaxUpdateIntervalMs = intervalMs; console.log(`AJAX обновление логов включено с интервалом ${intervalMs}ms`); // Запускаем первое обновление сразу performAjaxLogUpdate(); // Устанавливаем интервал ajaxUpdateInterval = setInterval(performAjaxLogUpdate, intervalMs); // Обновляем UI updateAjaxUpdateCheckbox(); // Обновляем видимость кнопки refresh и состояние кнопки update updateRefreshButtonVisibility(); } /** * Отключить периодическое обновление логов через AJAX */ function disableAjaxLogUpdate() { if (ajaxUpdateInterval) { clearInterval(ajaxUpdateInterval); ajaxUpdateInterval = null; } ajaxUpdateEnabled = false; console.log('AJAX обновление логов отключено'); // Обновляем UI updateAjaxUpdateCheckbox(); // Обновляем видимость кнопки refresh и состояние кнопки update updateRefreshButtonVisibility(); } /** * Переключить состояние AJAX обновления */ function toggleAjaxLogUpdate() { console.log('toggleAjaxLogUpdate: Текущее состояние ajaxUpdateEnabled =', ajaxUpdateEnabled); if (ajaxUpdateEnabled) { console.log('toggleAjaxLogUpdate: Отключаем AJAX update'); disableAjaxLogUpdate(); } else { console.log('toggleAjaxLogUpdate: Включаем AJAX update'); enableAjaxLogUpdate(ajaxUpdateIntervalMs); } console.log('toggleAjaxLogUpdate: Новое состояние ajaxUpdateEnabled =', ajaxUpdateEnabled); // Обновляем видимость кнопки refresh и состояние кнопки update при переключении updateRefreshButtonVisibility(); } /** * Выполнить обновление логов через AJAX */ async function performAjaxLogUpdate() { if (!ajaxUpdateEnabled) { return; } // Получаем значение tail, учитывая опцию "all" let tailLines = els.tail.value; if (tailLines === 'all') { tailLines = 'all'; // Оставляем как строку для API } else { tailLines = parseInt(tailLines) || 50; } try { const token = localStorage.getItem('access_token'); if (!token) { console.error('AJAX Update: No access token found'); return; } // Определяем контейнеры для обновления let containersToUpdate = []; if (state.multiViewMode && state.selectedContainers.length > 0) { // Multi-view режим: обновляем все выбранные контейнеры containersToUpdate = state.selectedContainers; } else if (state.current) { // Single-view режим: обновляем текущий контейнер containersToUpdate = [state.current.id]; } else { console.log('AJAX Update: Нет контейнеров для обновления'); return; } console.log(`AJAX Update: Обновляем ${containersToUpdate.length} контейнеров:`, containersToUpdate); // Обновляем каждый контейнер for (const containerId of containersToUpdate) { await updateContainerLogs(containerId, tailLines, token); } } catch (error) { console.error('AJAX Update Error:', error); // Не отключаем обновление при ошибке, просто логируем } } /** * Обновить логи для конкретного контейнера */ async function updateContainerLogs(containerId, tailLines, token) { try { // Формируем URL с параметрами const url = new URL(`/api/logs/${containerId}`, window.location.origin); // Передаем tail параметр как строку (для поддержки "all") url.searchParams.set('tail', String(tailLines)); // Получаем состояние контейнера const containerState = containerStates.get(containerId) || { lastTimestamp: null, lastSecondCount: 0 }; // Если у нас есть временная метка последнего обновления, используем её if (containerState.lastTimestamp) { url.searchParams.set('since', containerState.lastTimestamp); } console.log(`AJAX Update: Запрашиваем логи для ${containerId} с tail=${tailLines}`); // Формируем заголовки запроса const headers = { 'Authorization': `Bearer ${token}`, 'Cache-Control': 'no-cache' }; const response = await fetch(url.toString(), { headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } // Клиентская дедупликация: пропускаем уже учтённые строки в ту же секунду let newPortion = data.logs || []; // Извлекаем секундную часть из timestamp ответа сервера const serverTs = (data.timestamp || '').split('.')[0]; // отбрасываем миллисекунды if (!containerState.lastTimestamp || serverTs !== containerState.lastTimestamp) { // Новая секунда — сбрасываем счётчик containerState.lastTimestamp = serverTs; containerState.lastSecondCount = 0; } if (newPortion.length > 0) { // Обрезаем уже учтённые строки в той же секунде if (containerState.lastSecondCount > 0 && newPortion.length > containerState.lastSecondCount) { newPortion = newPortion.slice(containerState.lastSecondCount); } else if (containerState.lastSecondCount >= newPortion.length) { newPortion = []; } if (newPortion.length > 0) { console.log(`AJAX Update: К обработке ${newPortion.length} строк для ${containerId} (из ${data.logs.length}), lastSecondCount=${containerState.lastSecondCount}`); appendNewLogsForContainer(containerId, newPortion); containerState.lastSecondCount += newPortion.length; } else { console.log(`AJAX Update: Новых логов нет для ${containerId} после дедупликации по секундам`); } } else { console.log(`AJAX Update: Логи не пришли для ${containerId}`); } // Обновляем состояние контейнера containerStates.set(containerId, containerState); } catch (error) { console.error(`AJAX Update Error for ${containerId}:`, error); } } /** * Добавить новые логи в конец существующих (универсальная функция для single и multi view) * @param {string} containerId - ID контейнера * @param {Array} newLogs - Массив новых логов */ function appendNewLogsForContainer(containerId, newLogs) { const obj = state.open[containerId]; if (!obj) { console.warn(`AJAX Update: Object not found for container ${containerId}`); return; } // Обрабатываем каждую новую строку лога через handleLine let addedCount = 0; newLogs.forEach(log => { const message = log.message || log.raw || ''; if (message.trim()) { // Используем существующую функцию handleLine для правильной обработки handleLine(containerId, message); addedCount++; } }); // Прокручиваем к концу, если включена автопрокрутка if (state.autoScroll) { if (state.multiViewMode) { // Для multi-view прокручиваем все контейнеры const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (multiViewLog) { multiViewLog.scrollTop = multiViewLog.scrollHeight; } } else if (els.logContent) { // Для single-view прокручиваем основной контент els.logContent.scrollTop = els.logContent.scrollHeight; } } // Обновляем счетчики if (state.multiViewMode) { // Для multi-view обновляем счетчики конкретного контейнера updateContainerCounters(containerId); } else { // Для single-view обновляем общие счетчики recalculateCounters(); } // Очищаем дублированные строки if (state.multiViewMode) { const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`); if (multiViewLog) { cleanMultiViewDuplicateLines(multiViewLog); cleanMultiViewEmptyLines(multiViewLog); } } else { cleanDuplicateLines(els.logContent); cleanSingleViewEmptyLines(els.logContent); } console.log(`AJAX Update: Обработано ${addedCount} новых строк логов для ${containerId} через handleLine`); } /** * Добавить новые логи в конец существующих (для обратной совместимости) * @param {Array} newLogs - Массив новых логов */ function appendNewLogs(newLogs) { if (!state.current || !els.logContent) { return; } const containerId = state.current.id; appendNewLogsForContainer(containerId, newLogs); } /** * Обновить чекбокс AJAX обновления в UI */ function updateAjaxUpdateCheckbox() { const checkbox = document.getElementById('autoupdate'); if (checkbox) { checkbox.checked = ajaxUpdateEnabled; } // Обновляем видимость кнопки refresh в зависимости от состояния ajax autoupdate updateRefreshButtonVisibility(); } /** * Обновить видимость кнопки refresh в header и состояние кнопки update */ function updateRefreshButtonVisibility() { console.log('updateRefreshButtonVisibility: ajaxUpdateEnabled =', ajaxUpdateEnabled); const refreshButtons = document.querySelectorAll('.log-refresh-btn'); console.log('updateRefreshButtonVisibility: Найдено кнопок refresh =', refreshButtons.length); refreshButtons.forEach(btn => { if (ajaxUpdateEnabled) { // Если ajax autoupdate включен, скрываем кнопку refresh btn.style.display = 'none'; console.log('updateRefreshButtonVisibility: Скрываем кнопку refresh'); } else { // Если ajax autoupdate выключен, показываем кнопку refresh btn.style.display = 'inline-flex'; console.log('updateRefreshButtonVisibility: Показываем кнопку refresh'); } }); // Обновляем состояние кнопки update console.log('updateRefreshButtonVisibility: Обновляем состояние кнопки update'); setAjaxUpdateState(ajaxUpdateEnabled); } /** * Инициализировать чекбокс AJAX обновления */ function initAjaxUpdateCheckbox() { const checkbox = document.getElementById('autoupdate'); if (!checkbox) { console.error('AJAX Update Checkbox not found in HTML'); return; } // Настраиваем чекбокс checkbox.title = 'Автоматическое обновление логов через AJAX'; // Добавляем обработчик изменения checkbox.addEventListener('change', function() { if (this.checked) { enableAjaxLogUpdate(); } else { disableAjaxLogUpdate(); } // Обновляем видимость кнопки refresh и состояние кнопки update при изменении состояния чекбокса updateRefreshButtonVisibility(); }); // Устанавливаем начальное состояние (включен по умолчанию) checkbox.checked = true; ajaxUpdateEnabled = true; // Обновляем видимость кнопки refresh и состояние кнопки update при инициализации updateRefreshButtonVisibility(); console.log('AJAX Update Checkbox initialized'); } /** * Инициализация AJAX обновления */ async function initAjaxUpdate() { initAjaxUpdateCheckbox(); // Получаем настройки с сервера try { const token = localStorage.getItem('access_token'); if (token) { const response = await fetch('/api/settings', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const settings = await response.json(); ajaxUpdateIntervalMs = settings.ajax_update_interval || 2000; console.log(`AJAX Update: Интервал обновления получен с сервера: ${ajaxUpdateIntervalMs}ms`); } else { console.warn('AJAX Update: Не удалось получить настройки с сервера, используем значение по умолчанию'); ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию } } else { console.warn('AJAX Update: Токен не найден, используем значение по умолчанию'); ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию } } catch (error) { console.error('AJAX Update: Ошибка получения настроек:', error); ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию } console.log(`AJAX Update: Интервал обновления установлен на ${ajaxUpdateIntervalMs}ms`); // НЕ останавливаем AJAX обновление при смене контейнера const originalSwitchToSingle = window.switchToSingle; window.switchToSingle = function(containerId) { // Очищаем состояние для всех контейнеров containerStates.clear(); return originalSwitchToSingle.call(this, containerId); }; // НЕ останавливаем AJAX обновление при переключении в multi-view const originalSwitchToMultiView = window.switchToMultiView; window.switchToMultiView = function() { // Очищаем состояние для всех контейнеров containerStates.clear(); return originalSwitchToMultiView.call(this); }; console.log('AJAX обновление логов инициализировано'); // Обновляем видимость кнопки refresh и состояние кнопки update после инициализации updateRefreshButtonVisibility(); } // Запускаем инициализацию AJAX обновления initAjaxUpdate().then(() => { // Автоматически запускаем AJAX обновление (так как чекбокс включен по умолчанию) setTimeout(() => { if (ajaxUpdateEnabled) { console.log('AJAX Update: Автоматический запуск обновления логов'); enableAjaxLogUpdate(); } }, 1000); // Запускаем через 1 секунду после инициализации }); // Экспортируем функции в глобальную область для отладки window.enableAjaxLogUpdate = enableAjaxLogUpdate; window.disableAjaxLogUpdate = disableAjaxLogUpdate; window.toggleAjaxLogUpdate = toggleAjaxLogUpdate; window.performAjaxLogUpdate = performAjaxLogUpdate; window.updateContainerLogs = updateContainerLogs; // Добавляем обработчик изменения выбранных контейнеров в multi-view const originalToggleContainerSelection = window.toggleContainerSelection; window.toggleContainerSelection = function(containerId) { const result = originalToggleContainerSelection.call(this, containerId); // Если AJAX обновление активно, очищаем состояние для измененных контейнеров if (ajaxUpdateEnabled) { // Очищаем состояние для всех контейнеров, чтобы избежать проблем с синхронизацией containerStates.clear(); console.log('AJAX Update: Очищено состояние контейнеров после изменения выбора'); } return result; }; })();