docs: add comprehensive JSDoc comments to all JavaScript functions
This commit is contained in:
parent
a49714ab14
commit
f5926b80ad
@ -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();
|
||||
|
@ -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},
|
||||
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, // Режим мультипросмотра
|
||||
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: [
|
||||
/**
|
||||
* Настройки экземпляров контейнеров
|
||||
* Содержит цвета, фильтры и палитру для визуального различия контейнеров
|
||||
*/
|
||||
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);
|
||||
|
@ -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();
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user