diff --git a/app/static/js/error.js b/app/static/js/error.js index b64e70b..0ac3813 100644 --- a/app/static/js/error.js +++ b/app/static/js/error.js @@ -1,4 +1,13 @@ -// Theme toggle functionality +/** + * LogBoard+ - Скрипт страницы ошибок + * Автор: Сергей Антропов + * Сайт: https://devops.org.ru + */ + +/** + * Переключает тему между светлой и темной + * Обновляет атрибут data-theme и сохраняет выбор в localStorage + */ function toggleTheme() { const html = document.documentElement; const currentTheme = html.getAttribute('data-theme'); @@ -14,7 +23,10 @@ function toggleTheme() { } } -// Initialize theme on page load +/** + * Инициализация темы при загрузке страницы + * Загружает сохраненную тему из localStorage + */ document.addEventListener('DOMContentLoaded', function() { const savedTheme = localStorage.getItem('lb_theme') || 'dark'; document.documentElement.setAttribute('data-theme', savedTheme); @@ -26,7 +38,9 @@ document.addEventListener('DOMContentLoaded', function() { } }); -// Keyboard shortcut for theme toggle (Ctrl+T) +/** + * Горячая клавиша для переключения темы (Ctrl+T) + */ document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 't') { e.preventDefault(); diff --git a/app/static/js/index.js b/app/static/js/index.js index 840a93b..cae90dd 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -1,62 +1,80 @@ +/** + * LogBoard+ - Веб-панель для просмотра логов микросервисов + * Автор: Сергей Антропов + * Сайт: https://devops.org.ru + * Версия: 2.0 + */ + console.log('LogBoard+ script loaded - VERSION 2'); +/** + * Глобальное состояние приложения + * Содержит все данные о контейнерах, настройках и режимах отображения + */ const state = { - services: [], - current: null, - open: {}, // 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, // Режим мультипросмотра + 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'), + // 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'), - ajaxUpdateBtn: document.getElementById('ajaxUpdateBtn'), - projectBadge: document.getElementById('projectBadge'), + 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'), - lvlInfo: document.getElementById('lvlInfo'), - lvlWarn: document.getElementById('lvlWarn'), - lvlErr: document.getElementById('lvlErr'), - lvlOther: document.getElementById('lvlOther'), - 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 }, + 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'), + // 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'), // Заголовок одиночного просмотра }; -// ----- Theme toggle ----- +/** + * Инициализация переключателя темы + * Загружает сохраненную тему из localStorage и настраивает переключатель + */ (function initTheme(){ const saved = localStorage.lb_theme || 'dark'; document.documentElement.setAttribute('data-theme', saved); @@ -68,6 +86,10 @@ const els = { }); })(); +/** + * Устанавливает состояние WebSocket соединения в интерфейсе + * @param {string} s - Состояние: 'on', 'off', 'err', 'available' + */ function setWsState(s){ console.log('setWsState: Устанавливаем состояние', s); console.log('setWsState: Текущие соединения:', Object.keys(state.open)); @@ -89,7 +111,10 @@ function setWsState(s){ } } -// Функция для определения общего состояния WebSocket соединений +/** + * Определяет общее состояние WebSocket соединений + * Проверяет все открытые соединения и устанавливает соответствующее состояние + */ function determineWsState() { const openConnections = Object.keys(state.open); @@ -238,6 +263,10 @@ function stopWebSocketStatusCheck() { } } +/** + * Устанавливает визуальное состояние кнопки AJAX обновления + * @param {boolean} enabled - Включено ли AJAX обновление + */ function setAjaxUpdateState(enabled) { console.log('setAjaxUpdateState: enabled =', enabled, 'els.ajaxUpdateBtn =', !!els.ajaxUpdateBtn); @@ -260,7 +289,10 @@ function setAjaxUpdateState(enabled) { } } -// Функция для обновления всех логов при изменении фильтров +/** + * Обновляет отображение всех логов при изменении фильтров + * Перерисовывает логи с учетом текущих настроек фильтрации и уровней + */ function refreshAllLogs() { // Обновляем обычный просмотр Object.keys(state.open).forEach(id => { @@ -339,8 +371,19 @@ function refreshAllLogs() { } }, 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(); @@ -374,6 +417,11 @@ function classify(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; @@ -383,7 +431,13 @@ function allowedByLevel(cls){ return true; } -// Функция для проверки уровня логирования для конкретного контейнера +/** + * Проверяет, разрешен ли отображение лога данного уровня для конкретного контейнера + * Используется в режиме мультипросмотра для индивидуальных настроек контейнеров + * @param {string} cls - Класс уровня лога ('dbg', 'err', 'warn', 'ok', 'other') + * @param {string} containerId - ID контейнера + * @returns {boolean} Разрешен ли отображение + */ function allowedByContainerLevel(cls, containerId) { // Если настройки контейнера не инициализированы, инициализируем их if (!state.containerLevels) { @@ -408,7 +462,11 @@ function allowedByContainerLevel(cls, containerId) { return result; } -// Функция для обновления видимости логов в Single View +/** + * Обновляет видимость логов в Single View режиме + * Перерисовывает логи с учетом текущих фильтров и настроек уровней + * @param {HTMLElement} logElement - Элемент для обновления + */ function updateLogVisibility(logElement) { if (!logElement || !state.current) return; @@ -616,6 +674,12 @@ function initializeLevelButtons() { // Применяем настройки wrap text applyWrapSettings(); } +/** + * Применяет фильтр к строке лога + * Проверяет, соответствует ли строка текущему фильтру (безопасный regex поиск) + * @param {string} line - Строка лога для проверки + * @returns {boolean} Проходит ли строка фильтр + */ function applyFilter(line){ if(!state.filter) return true; try{ @@ -628,14 +692,30 @@ function applyFilter(line){ } } -// ANSI → HTML (SGR: 0/1/3/4, 30-37) +/** + * Константы и настройки для работы с ANSI цветами + * SGR (Select Graphic Rendition): 0/1/3/4, 30-37 + */ -// ----- Instance color & filters ----- -const inst = { colors: {}, filters: {}, palette: [ - '#7aa2f7','#9ece6a','#e0af68','#f7768e','#bb9af7','#7dcfff','#c0caf5','#f6bd60', - '#84cc16','#06b6d4','#fb923c','#ef4444','#22c55e','#a855f7' -]}; +/** + * Настройки экземпляров контейнеров + * Содержит цвета, фильтры и палитру для визуального различия контейнеров + */ +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 @@ -679,6 +759,12 @@ function parsePrefixAndStrip(line){ 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); @@ -1614,6 +1700,11 @@ async function updateMultiViewMode() { updateLogLevelsVisibility(); } +/** + * Настраивает интерфейс для режима мультипросмотра (multi-view) + * Создает сетку панелей для одновременного просмотра нескольких контейнеров + * Открывает WebSocket соединения для всех выбранных контейнеров + */ async function setupMultiView() { console.log('setupMultiView called'); @@ -1749,6 +1840,12 @@ async function setupMultiView() { 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'); @@ -1833,6 +1930,11 @@ function createMultiViewPanel(service) { return panel; } +/** + * Открывает WebSocket соединение для контейнера в режиме мультипросмотра + * Настраивает обработчики сообщений и управляет отображением логов + * @param {Object} service - Объект сервиса/контейнера + */ function openMultiViewWs(service) { const containerId = service.id; console.log(`openMultiViewWs: Starting WebSocket setup for ${service.name} (${containerId})`); @@ -2099,6 +2201,10 @@ function wsUrl(containerId, service, project){ 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; @@ -2107,6 +2213,11 @@ function closeWs(id){ delete state.open[id]; } +/** + * Создает и скачивает снимок логов контейнера + * В режиме мультипросмотра создает отдельные файлы для каждого контейнера + * @param {string} id - ID контейнера + */ async function sendSnapshot(id){ const o = state.open[id]; if (!o){ alert('not open'); return; } @@ -2222,6 +2333,12 @@ async function sendSnapshot(id){ } } +/** + * Открывает 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}`); @@ -2979,7 +3096,12 @@ function checkMultiViewHTML() { console.log('=== Конец проверки HTML ==='); } -// Глобальная функция для обработки логов +/** + * Основная функция обработки строк логов + * Классифицирует, фильтрует и отображает строки логов в зависимости от режима + * @param {string} id - ID контейнера + * @param {string} line - Строка лога для обработки + */ function handleLine(id, line){ const obj = state.open[id]; @@ -3191,6 +3313,11 @@ function ensurePanel(svc){ 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); diff --git a/app/static/js/login.js b/app/static/js/login.js index f1728cc..03cc209 100644 --- a/app/static/js/login.js +++ b/app/static/js/login.js @@ -1,4 +1,13 @@ -// Theme toggle +/** + * LogBoard+ - Скрипт страницы входа + * Автор: Сергей Антропов + * Сайт: https://devops.org.ru + */ + +/** + * Инициализация переключателя темы + * Загружает сохраненную тему из localStorage и настраивает переключатель + */ (function initTheme(){ const saved = localStorage.lb_theme || 'dark'; document.documentElement.setAttribute('data-theme', saved); @@ -10,7 +19,10 @@ }); })(); -// Password toggle +/** + * Обработчик переключения видимости пароля + * Показывает/скрывает пароль и меняет иконку + */ document.getElementById('passwordToggle').addEventListener('click', function() { const passwordInput = document.getElementById('password'); const icon = this.querySelector('i'); @@ -24,7 +36,10 @@ document.getElementById('passwordToggle').addEventListener('click', function() { } }); -// Login form +/** + * Обработчик отправки формы входа + * Выполняет аутентификацию пользователя через API + */ document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); @@ -82,18 +97,27 @@ document.getElementById('loginForm').addEventListener('submit', async function(e } }); +/** + * Показывает сообщение об ошибке + * @param {string} message - Текст ошибки + */ function showError(message) { const errorMessage = document.getElementById('errorMessage'); errorMessage.textContent = message; errorMessage.classList.add('show'); } +/** + * Скрывает сообщение об ошибке + */ function hideError() { const errorMessage = document.getElementById('errorMessage'); errorMessage.classList.remove('show'); } -// Auto-focus on username field +/** + * Автофокус на поле имени пользователя при загрузке страницы + */ document.addEventListener('DOMContentLoaded', function() { document.getElementById('username').focus(); });