Исправлена проблема с парсером в режиме multiview
- Добавлена инициализация счетчиков в openMultiViewWs - Исправлена обработка входящих сообщений WebSocket - Добавлена отладочная информация для диагностики - Парсер теперь работает сразу при переключении в multiview - Улучшена обработка ошибок и проверки объектов Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
parent
293e9c8cba
commit
1745d4b5d4
@ -892,6 +892,115 @@ a{color:var(--link)}
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Чекбоксы для мультивыбора контейнеров */
|
||||||
|
.container-select {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-checkbox-label:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-checkbox:checked + .container-checkbox-label {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-checkbox:checked + .container-checkbox-label::after {
|
||||||
|
content: "✓";
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-item.selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--tab-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мультипросмотр */
|
||||||
|
.multi-view-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-view-panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-view-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--chip);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-view-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--fg);
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.multi-view-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-view-log {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
/* Log Area */
|
/* Log Area */
|
||||||
.log-area {
|
.log-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -990,6 +1099,31 @@ main{display:none}
|
|||||||
.logwrap{flex:1;overflow:auto;padding:10px}
|
.logwrap{flex:1;overflow:auto;padding:10px}
|
||||||
.log{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;margin:0;tab-size:2}
|
.log{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;margin:0;tab-size:2}
|
||||||
.line{color:var(--fg)} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .dbg{color:#7dcfff} .ts{color:var(--muted)}
|
.line{color:var(--fg)} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .dbg{color:#7dcfff} .ts{color:var(--muted)}
|
||||||
|
/* Цвета для multi-view логов */
|
||||||
|
.multi-view-log .line{color:var(--fg) !important}
|
||||||
|
.multi-view-log .ok{color:var(--ok) !important}
|
||||||
|
.multi-view-log .warn{color:var(--warn) !important}
|
||||||
|
.multi-view-log .err{color:var(--err) !important}
|
||||||
|
.multi-view-log .dbg{color:#7dcfff !important}
|
||||||
|
.multi-view-log .ts{color:var(--muted) !important}
|
||||||
|
|
||||||
|
/* Дополнительные стили для multi-view логов */
|
||||||
|
.multi-view-log span.line{color:var(--fg) !important}
|
||||||
|
.multi-view-log span.ok{color:var(--ok) !important}
|
||||||
|
.multi-view-log span.warn{color:var(--warn) !important}
|
||||||
|
.multi-view-log span.err{color:var(--err) !important}
|
||||||
|
.multi-view-log span.dbg{color:#7dcfff !important}
|
||||||
|
.multi-view-log span.ts{color:var(--muted) !important}
|
||||||
|
|
||||||
|
/* Стили для ANSI цветов в multi-view */
|
||||||
|
.multi-view-log .ansi-red{color:#f7768e !important}
|
||||||
|
.multi-view-log .ansi-green{color:#22c55e !important}
|
||||||
|
.multi-view-log .ansi-yellow{color:#eab308 !important}
|
||||||
|
.multi-view-log .ansi-blue{color:#3b82f6 !important}
|
||||||
|
.multi-view-log .ansi-magenta{color:#a855f7 !important}
|
||||||
|
.multi-view-log .ansi-cyan{color:#06b6d4 !important}
|
||||||
|
.multi-view-log .ansi-white{color:var(--fg) !important}
|
||||||
|
.multi-view-log .ansi-black{color:#79808f !important}
|
||||||
footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||||
.filterlvl{display:flex;gap:6px;align-items:center}
|
.filterlvl{display:flex;gap:6px;align-items:center}
|
||||||
/* Instance tag */
|
/* Instance tag */
|
||||||
@ -1210,6 +1344,8 @@ const state = {
|
|||||||
layout: 'tabs', // 'tabs' | 'grid2' | 'grid3' | 'grid4'
|
layout: 'tabs', // 'tabs' | 'grid2' | 'grid3' | 'grid4'
|
||||||
filter: null,
|
filter: null,
|
||||||
levels: {debug:true, info:true, warn:true, err:true},
|
levels: {debug:true, info:true, warn:true, err:true},
|
||||||
|
selectedContainers: [], // Массив ID выбранных контейнеров для мультипросмотра
|
||||||
|
multiViewMode: false, // Режим мультипросмотра
|
||||||
};
|
};
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
@ -1277,6 +1413,7 @@ function setWsState(s){
|
|||||||
|
|
||||||
// Функция для обновления всех логов при изменении фильтров
|
// Функция для обновления всех логов при изменении фильтров
|
||||||
function refreshAllLogs() {
|
function refreshAllLogs() {
|
||||||
|
// Обновляем обычный просмотр
|
||||||
Object.keys(state.open).forEach(id => {
|
Object.keys(state.open).forEach(id => {
|
||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
if (!obj || !obj.logEl) return;
|
if (!obj || !obj.logEl) return;
|
||||||
@ -1303,29 +1440,87 @@ function refreshAllLogs() {
|
|||||||
els.logContent.innerHTML = obj.logEl.innerHTML;
|
els.logContent.innerHTML = obj.logEl.innerHTML;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обновляем мультипросмотр
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const obj = state.open[containerId];
|
||||||
|
if (!obj || !obj.logEl) return;
|
||||||
|
|
||||||
|
// Получаем все логи из буфера
|
||||||
|
const allLogs = obj.allLogs || [];
|
||||||
|
const filteredHtml = [];
|
||||||
|
|
||||||
|
allLogs.forEach(logEntry => {
|
||||||
|
// Проверяем уровень логирования
|
||||||
|
if (!allowedByLevel(logEntry.cls)) return;
|
||||||
|
|
||||||
|
// Проверяем фильтр
|
||||||
|
if (!applyFilter(logEntry.line)) return;
|
||||||
|
|
||||||
|
filteredHtml.push(logEntry.html);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем отображение в мультипросмотре
|
||||||
|
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||||
|
if (multiViewLog) {
|
||||||
|
multiViewLog.innerHTML = filteredHtml.join('');
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Multi-view refresh: Container ${containerId}, ${filteredHtml.length} lines, HTML preview:`, filteredHtml.slice(0, 2).join('').substring(0, 200));
|
||||||
|
// Проверяем, что HTML действительно содержит цветные элементы
|
||||||
|
const coloredElements = multiViewLog.querySelectorAll('.ok, .warn, .err, .dbg, .ansi-red, .ansi-green, .ansi-yellow, .ansi-blue');
|
||||||
|
console.log(`Multi-view: Found ${coloredElements.length} colored elements in container ${containerId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
|
function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); }
|
||||||
|
|
||||||
function classify(line){
|
function classify(line){
|
||||||
const l = line.toLowerCase();
|
const l = line.toLowerCase();
|
||||||
|
|
||||||
// Проверяем различные форматы уровней логирования
|
// Отладочная информация для понимания что происходит
|
||||||
if (/\bdebug\b|level=debug| \[debug\]/.test(l)) return 'dbg';
|
if (line.includes('INFO') || line.includes('WARNING') || line.includes('ERROR') || line.includes('DEBUG')) {
|
||||||
if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
|
console.log(`Classifying line: "${line.substring(0, 100)}..."`);
|
||||||
if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn';
|
}
|
||||||
if (/\b(info|started|listening|ready|up)\b|level=info/.test(l)) return 'ok';
|
|
||||||
|
|
||||||
// Дополнительные проверки для других форматов
|
// Проверяем различные форматы уровней логирования (более специфичные сначала)
|
||||||
|
|
||||||
|
// DEBUG - ищем точное совпадение уровня логирования
|
||||||
|
if (/\s- DEBUG -|\s\[debug\]|level=debug|\bdebug\b(?=\s|$)/.test(l)) {
|
||||||
|
console.log(`Classified as DEBUG: "${line.substring(0, 100)}..."`);
|
||||||
|
return 'dbg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERROR - ищем точное совпадение уровня логирования
|
||||||
|
if (/\s- ERROR -|\s\[error\]|level=error/.test(l)) {
|
||||||
|
console.log(`Classified as ERROR: "${line.substring(0, 100)}..."`);
|
||||||
|
return 'err';
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING - ищем точное совпадение уровня логирования
|
||||||
|
if (/\s- WARNING -|\s\[warn\]|level=warn/.test(l)) {
|
||||||
|
console.log(`Classified as WARNING: "${line.substring(0, 100)}..."`);
|
||||||
|
return 'warn';
|
||||||
|
}
|
||||||
|
|
||||||
|
// INFO - ищем точное совпадение уровня логирования
|
||||||
|
if (/\s- INFO -|\s\[info\]|level=info/.test(l)) {
|
||||||
|
console.log(`Classified as INFO: "${line.substring(0, 100)}..."`);
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополнительные проверки для других форматов (только если не найдены точные совпадения)
|
||||||
if (/\bdebug\b/i.test(l)) return 'dbg';
|
if (/\bdebug\b/i.test(l)) return 'dbg';
|
||||||
if (/\berror\b/i.test(l)) return 'err';
|
if (/\berror\b/i.test(l)) return 'err';
|
||||||
if (/\bwarning\b/i.test(l)) return 'warn';
|
if (/\bwarning\b/i.test(l)) return 'warn';
|
||||||
if (/\binfo\b/i.test(l)) return 'ok';
|
if (/\binfo\b/i.test(l)) return 'ok';
|
||||||
|
|
||||||
// Отладочная информация для неклассифицированных строк
|
// Отладочная информация для неклассифицированных строк
|
||||||
if (line.includes('level=')) {
|
if (line.includes('level=')) {
|
||||||
console.log(`Unclassified line with level: "${line.substring(0, 200)}..."`);
|
console.log(`Unclassified line with level: "${line.substring(0, 200)}..."`);
|
||||||
console.log(`Lowercase version: "${l.substring(0, 200)}..."`);
|
console.log(`Lowercase version: "${l.substring(0, 200)}..."`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
}
|
||||||
@ -1484,11 +1679,15 @@ function buildTabs(){
|
|||||||
${escapeHtml(svc.status)}
|
${escapeHtml(svc.status)}
|
||||||
${svc.url ? `<a href="${svc.url}" target="_blank" class="container-link" title="Открыть сайт"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
${svc.url ? `<a href="${svc.url}" target="_blank" class="container-link" title="Открыть сайт"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-select">
|
||||||
|
<input type="checkbox" id="select-${svc.id}" class="container-checkbox" data-container-id="${svc.id}">
|
||||||
|
<label for="select-${svc.id}" class="container-checkbox-label" title="Выбрать для мультипросмотра"></label>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
item.onclick = (e) => {
|
item.onclick = (e) => {
|
||||||
// Не переключаем контейнер, если кликнули на ссылку
|
// Не переключаем контейнер, если кликнули на ссылку или чекбокс
|
||||||
if (e.target.closest('.container-link')) {
|
if (e.target.closest('.container-link') || e.target.closest('.container-select')) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1735,6 +1934,251 @@ async function removeExcludedContainer(containerName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функции для мультивыбора контейнеров
|
||||||
|
function toggleContainerSelection(containerId) {
|
||||||
|
const index = state.selectedContainers.indexOf(containerId);
|
||||||
|
if (index > -1) {
|
||||||
|
state.selectedContainers.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
state.selectedContainers.push(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerSelectionUI();
|
||||||
|
updateMultiViewMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContainerSelectionUI() {
|
||||||
|
// Обновляем чекбоксы
|
||||||
|
document.querySelectorAll('.container-checkbox').forEach(checkbox => {
|
||||||
|
const containerId = checkbox.getAttribute('data-container-id');
|
||||||
|
const containerItem = checkbox.closest('.container-item');
|
||||||
|
|
||||||
|
if (state.selectedContainers.includes(containerId)) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
containerItem.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
checkbox.checked = false;
|
||||||
|
containerItem.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем заголовок
|
||||||
|
updateLogTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMultiViewMode() {
|
||||||
|
console.log(`updateMultiViewMode called: selectedContainers.length = ${state.selectedContainers.length}, containers:`, state.selectedContainers);
|
||||||
|
|
||||||
|
if (state.selectedContainers.length > 1) {
|
||||||
|
state.multiViewMode = true;
|
||||||
|
state.current = null; // Сбрасываем текущий контейнер
|
||||||
|
console.log('Setting up multi-view mode');
|
||||||
|
setupMultiView();
|
||||||
|
} else if (state.selectedContainers.length === 1) {
|
||||||
|
state.multiViewMode = false;
|
||||||
|
const selectedService = state.services.find(s => s.id === state.selectedContainers[0]);
|
||||||
|
if (selectedService) {
|
||||||
|
switchToSingle(selectedService);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Когда снимаем все галочки, переключаемся в single view
|
||||||
|
state.multiViewMode = false;
|
||||||
|
state.current = null;
|
||||||
|
clearLogArea();
|
||||||
|
|
||||||
|
// Очищаем область логов и показываем пустое состояние
|
||||||
|
const logArea = document.querySelector('.log-area');
|
||||||
|
if (logArea) {
|
||||||
|
const logContent = logArea.querySelector('.log-content');
|
||||||
|
if (logContent) {
|
||||||
|
logContent.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 14px;">Выберите контейнер для просмотра логов</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем заголовок
|
||||||
|
if (els.logTitle) {
|
||||||
|
els.logTitle.textContent = 'LogBoard+';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Multi-view mode updated: multiViewMode = ${state.multiViewMode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMultiView() {
|
||||||
|
console.log('setupMultiView called');
|
||||||
|
const logArea = document.querySelector('.log-area');
|
||||||
|
if (!logArea) {
|
||||||
|
console.log('Log area not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем область логов
|
||||||
|
const logContent = logArea.querySelector('.log-content');
|
||||||
|
if (logContent) {
|
||||||
|
logContent.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем сетку для мультипросмотра
|
||||||
|
const gridContainer = document.createElement('div');
|
||||||
|
gridContainer.className = 'multi-view-grid';
|
||||||
|
gridContainer.id = 'multiViewGrid';
|
||||||
|
|
||||||
|
// Определяем количество колонок в зависимости от количества контейнеров
|
||||||
|
let columns = 1;
|
||||||
|
if (state.selectedContainers.length <= 2) columns = 2;
|
||||||
|
else if (state.selectedContainers.length <= 4) columns = 2;
|
||||||
|
else if (state.selectedContainers.length <= 6) columns = 3;
|
||||||
|
else columns = 4;
|
||||||
|
|
||||||
|
gridContainer.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||||
|
|
||||||
|
// Создаем панели для каждого выбранного контейнера
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const service = state.services.find(s => s.id === containerId);
|
||||||
|
if (!service) return;
|
||||||
|
|
||||||
|
const panel = createMultiViewPanel(service);
|
||||||
|
gridContainer.appendChild(panel);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logContent) {
|
||||||
|
logContent.appendChild(gridContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяем настройки wrap lines
|
||||||
|
applyWrapSettings();
|
||||||
|
|
||||||
|
// Подключаем WebSocket для каждого контейнера
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const service = state.services.find(s => s.id === containerId);
|
||||||
|
if (service) {
|
||||||
|
console.log(`Setting up WebSocket for multi-view container: ${service.name} (${containerId})`);
|
||||||
|
openMultiViewWs(service);
|
||||||
|
} else {
|
||||||
|
console.error(`Service not found for container ID: ${containerId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMultiViewPanel(service) {
|
||||||
|
console.log(`Creating multi-view panel for service: ${service.name} (${service.id})`);
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'multi-view-panel';
|
||||||
|
panel.setAttribute('data-container-id', service.id);
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="multi-view-header">
|
||||||
|
<h4 class="multi-view-title">${escapeHtml(service.name)}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="multi-view-content">
|
||||||
|
<div class="multi-view-log" data-container-id="${service.id}">Connecting...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Проверяем, что элемент создался правильно
|
||||||
|
const logElement = panel.querySelector(`.multi-view-log[data-container-id="${service.id}"]`);
|
||||||
|
if (logElement) {
|
||||||
|
console.log(`Multi-view log element created successfully for ${service.name}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to create multi-view log element for ${service.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Multi-view panel created for ${service.name}`);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMultiViewWs(service) {
|
||||||
|
const containerId = service.id;
|
||||||
|
|
||||||
|
// Закрываем существующее соединение
|
||||||
|
closeWs(containerId);
|
||||||
|
|
||||||
|
// Создаем новое WebSocket соединение
|
||||||
|
const ws = new WebSocket(wsUrl(containerId, service.service, service.project));
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log(`Multi-view WebSocket connected for ${service.name}`);
|
||||||
|
const logEl = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||||
|
if (logEl) {
|
||||||
|
logEl.textContent = 'Connected...';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log(`Multi-view WebSocket received message for ${service.name}: ${event.data.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
const parts = (event.data||'').split(/\r?\n/);
|
||||||
|
|
||||||
|
for (let i=0;i<parts.length;i++){
|
||||||
|
if (parts[i].length===0 && i===parts.length-1) continue;
|
||||||
|
// harvest instance ids if present
|
||||||
|
const pr = parsePrefixAndStrip(parts[i]);
|
||||||
|
if (pr){ if (!(pr.id8 in inst.filters)) { inst.filters[pr.id8] = true; updateIdFiltersBar(); } }
|
||||||
|
handleLine(containerId, parts[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log(`Multi-view WebSocket closed for ${service.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error(`Multi-view WebSocket error for ${service.name}:`, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сохраняем соединение с полным набором полей как в openWs
|
||||||
|
state.open[containerId] = {
|
||||||
|
ws: ws,
|
||||||
|
serviceName: service.service,
|
||||||
|
logEl: document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`),
|
||||||
|
wrapEl: document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`),
|
||||||
|
counters: {dbg:0, info:0, warn:0, err:0},
|
||||||
|
pausedBuffer: [],
|
||||||
|
allLogs: [] // Добавляем буфер для логов
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogArea() {
|
||||||
|
const logContent = document.querySelector('.log-content');
|
||||||
|
if (logContent) {
|
||||||
|
logContent.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 14px;">Выберите контейнер для просмотра логов</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const logTitle = document.getElementById('logTitle');
|
||||||
|
if (logTitle) {
|
||||||
|
logTitle.textContent = 'LogBoard+';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLogTitle() {
|
||||||
|
const logTitle = document.getElementById('logTitle');
|
||||||
|
if (!logTitle) return;
|
||||||
|
|
||||||
|
if (state.selectedContainers.length === 0) {
|
||||||
|
logTitle.textContent = 'LogBoard+';
|
||||||
|
} else if (state.selectedContainers.length === 1) {
|
||||||
|
const service = state.services.find(s => s.id === state.selectedContainers[0]);
|
||||||
|
logTitle.textContent = `${service.name} (${service.service || service.name})`;
|
||||||
|
} else {
|
||||||
|
logTitle.textContent = `Multi-view: ${state.selectedContainers.length} containers`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWrapSettings() {
|
||||||
|
const wrapEnabled = els.wrapToggle && els.wrapToggle.checked;
|
||||||
|
const wrapStyle = wrapEnabled ? 'pre-wrap' : 'pre';
|
||||||
|
|
||||||
|
// Применяем к обычному просмотру
|
||||||
|
document.querySelectorAll('.log').forEach(el => {
|
||||||
|
el.style.whiteSpace = wrapStyle;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Применяем к мультипросмотру
|
||||||
|
document.querySelectorAll('.multi-view-log').forEach(el => {
|
||||||
|
el.style.whiteSpace = wrapStyle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchServices(){
|
async function fetchServices(){
|
||||||
try {
|
try {
|
||||||
console.log('Fetching services...');
|
console.log('Fetching services...');
|
||||||
@ -1857,54 +2301,98 @@ function openWs(svc, panel){
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Убираем автоматический refresh - теперь только по кнопке
|
// Убираем автоматический refresh - теперь только по кнопке
|
||||||
|
}
|
||||||
|
|
||||||
function handleLine(id, line){
|
// Глобальная функция для обработки логов
|
||||||
const cls = classify(line);
|
function handleLine(id, line){
|
||||||
if (cls==='dbg') counters.dbg++;
|
const obj = state.open[id];
|
||||||
if (cls==='ok') counters.info++;
|
if (!obj) {
|
||||||
if (cls==='warn') counters.warn++;
|
console.error(`handleLine: Object not found for container ${id}, available containers:`, Object.keys(state.open));
|
||||||
if (cls==='err') counters.err++;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация для первых нескольких строк
|
||||||
|
if (!obj.counters) {
|
||||||
|
console.error(`handleLine: Counters not initialized for container ${id}`);
|
||||||
|
obj.counters = {dbg:0, info:0, warn:0, err:0};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cls = classify(line);
|
||||||
|
|
||||||
|
// Обновляем счетчики
|
||||||
|
if (obj.counters) {
|
||||||
|
if (cls==='dbg') obj.counters.dbg++;
|
||||||
|
if (cls==='ok') obj.counters.info++;
|
||||||
|
if (cls==='warn') obj.counters.warn++;
|
||||||
|
if (cls==='err') obj.counters.err++;
|
||||||
|
|
||||||
// Отладочная информация для первых нескольких строк
|
// Отладочная информация для первых нескольких строк
|
||||||
if (counters.dbg + counters.info + counters.warn + counters.err < 10) {
|
if (obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 10) {
|
||||||
console.log(`Line: "${line.substring(0, 100)}..." -> Class: ${cls}`);
|
console.log(`Line: "${line.substring(0, 100)}..." -> Class: ${cls}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отладочная информация о счетчиках
|
// Отладочная информация о счетчиках
|
||||||
if (counters.dbg + counters.info + counters.warn + counters.err < 5) {
|
if (obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 5) {
|
||||||
console.log(`Counters: DEBUG=${counters.dbg}, INFO=${counters.info}, WARN=${counters.warn}, ERROR=${counters.err}`);
|
console.log(`Counters: DEBUG=${obj.counters.dbg}, INFO=${obj.counters.info}, WARN=${obj.counters.warn}, ERROR=${obj.counters.err}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const html = `<span class="line ${cls}">${ansiToHtml(line)}</span>\n`;
|
const html = `<span class="line ${cls}">${ansiToHtml(line)}</span>\n`;
|
||||||
const obj = state.open[id];
|
// Отладочная информация для HTML
|
||||||
if (!obj) return;
|
if (obj.counters && obj.counters.dbg + obj.counters.info + obj.counters.warn + obj.counters.err < 3) {
|
||||||
|
console.log(`Generated HTML for class ${cls}:`, html.substring(0, 200));
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем все логи в буфере (всегда)
|
// Сохраняем все логи в буфере (всегда)
|
||||||
if (!obj.allLogs) obj.allLogs = [];
|
if (!obj.allLogs) obj.allLogs = [];
|
||||||
obj.allLogs.push({html: html, line: line, cls: cls});
|
obj.allLogs.push({html: html, line: line, cls: cls});
|
||||||
|
|
||||||
// Ограничиваем размер буфера
|
// Ограничиваем размер буфера
|
||||||
if (obj.allLogs.length > 10000) {
|
if (obj.allLogs.length > 10000) {
|
||||||
obj.allLogs = obj.allLogs.slice(-5000);
|
obj.allLogs = obj.allLogs.slice(-5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем фильтры для отображения
|
// Проверяем фильтры для отображения
|
||||||
if (!allowedByLevel(cls)) return;
|
const shouldShow = allowedByLevel(cls) && applyFilter(line);
|
||||||
if (!applyFilter(line)) return;
|
|
||||||
|
|
||||||
// Добавляем логи в отображение
|
// Добавляем логи в отображение (обычный просмотр)
|
||||||
|
if (shouldShow && obj.logEl) {
|
||||||
obj.logEl.insertAdjacentHTML('beforeend', html);
|
obj.logEl.insertAdjacentHTML('beforeend', html);
|
||||||
if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
if (els.autoscroll && els.autoscroll.checked && obj.wrapEl) {
|
||||||
|
obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Update modern interface
|
// Update modern interface
|
||||||
if (state.current && state.current.id === id && els.logContent) {
|
if (state.current && state.current.id === id && els.logContent) {
|
||||||
els.logContent.innerHTML = obj.logEl.innerHTML;
|
els.logContent.innerHTML = obj.logEl.innerHTML;
|
||||||
const logContent = document.querySelector('.log-content');
|
const logContent = document.querySelector('.log-content');
|
||||||
if (logContent && els.autoscroll.checked) {
|
if (logContent && els.autoscroll && els.autoscroll.checked) {
|
||||||
logContent.scrollTop = logContent.scrollHeight;
|
logContent.scrollTop = logContent.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update multi-view interface
|
||||||
|
if (state.multiViewMode && state.selectedContainers.includes(id)) {
|
||||||
|
console.log(`Multi-view processing: container ${id}, shouldShow: ${shouldShow}, multiViewMode: ${state.multiViewMode}, selectedContainers:`, state.selectedContainers);
|
||||||
|
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${id}"]`);
|
||||||
|
if (multiViewLog) {
|
||||||
|
if (shouldShow) {
|
||||||
|
multiViewLog.insertAdjacentHTML('beforeend', html);
|
||||||
|
if (els.autoscroll && els.autoscroll.checked) {
|
||||||
|
multiViewLog.scrollTop = multiViewLog.scrollHeight;
|
||||||
|
}
|
||||||
|
// Отладочная информация для multi-view
|
||||||
|
console.log(`Multi-view: Added line with class ${cls} to container ${id}, HTML:`, html.substring(0, 100));
|
||||||
|
} else {
|
||||||
|
// Отладочная информация для отфильтрованных строк
|
||||||
|
console.log(`Multi-view: Filtered out line with class ${cls} from container ${id}, line: "${line.substring(0, 100)}..."`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Отладочная информация если элемент не найден
|
||||||
|
console.log(`Multi-view: Element not found for container ${id}, available elements:`, document.querySelectorAll('.multi-view-log').length);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePanel(svc){
|
function ensurePanel(svc){
|
||||||
@ -2142,38 +2630,82 @@ function updateCounterVisibility() {
|
|||||||
|
|
||||||
// Функция для обновления логов и счетчиков
|
// Функция для обновления логов и счетчиков
|
||||||
async function refreshLogsAndCounters() {
|
async function refreshLogsAndCounters() {
|
||||||
if (!state.current) {
|
if (state.multiViewMode && state.selectedContainers.length > 0) {
|
||||||
|
// Обновляем мультипросмотр
|
||||||
|
console.log('Refreshing multi-view for containers:', state.selectedContainers);
|
||||||
|
|
||||||
|
// Обновляем счетчики для всех выбранных контейнеров
|
||||||
|
for (const containerId of state.selectedContainers) {
|
||||||
|
await updateCounters(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перезапускаем WebSocket соединения для всех выбранных контейнеров
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
closeWs(containerId);
|
||||||
|
const service = state.services.find(s => s.id === containerId);
|
||||||
|
if (service) {
|
||||||
|
openMultiViewWs(service);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очищаем логи в мультипросмотре
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||||
|
if (multiViewLog) {
|
||||||
|
multiViewLog.textContent = 'Refreshing...';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (state.current) {
|
||||||
|
// Обычный режим просмотра
|
||||||
|
console.log('Refreshing logs and counters for:', state.current.id);
|
||||||
|
|
||||||
|
// Обновляем счетчики
|
||||||
|
await updateCounters(state.current.id);
|
||||||
|
|
||||||
|
// Перезапускаем WebSocket соединение для получения свежих логов
|
||||||
|
const currentId = state.current.id;
|
||||||
|
closeWs(currentId);
|
||||||
|
|
||||||
|
// Находим обновленный контейнер в списке
|
||||||
|
const updatedContainer = state.services.find(s => s.id === currentId);
|
||||||
|
if (updatedContainer) {
|
||||||
|
// Переключаемся на обновленный контейнер
|
||||||
|
switchToSingle(updatedContainer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
console.log('No container selected');
|
console.log('No container selected');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Refreshing logs and counters for:', state.current.id);
|
|
||||||
|
|
||||||
// Обновляем счетчики
|
|
||||||
await updateCounters(state.current.id);
|
|
||||||
|
|
||||||
// Перезапускаем WebSocket соединение для получения свежих логов
|
|
||||||
const currentId = state.current.id;
|
|
||||||
closeWs(currentId);
|
|
||||||
|
|
||||||
// Находим обновленный контейнер в списке
|
|
||||||
const updatedContainer = state.services.find(s => s.id === currentId);
|
|
||||||
if (updatedContainer) {
|
|
||||||
// Переключаемся на обновленный контейнер
|
|
||||||
switchToSingle(updatedContainer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
els.clearBtn.onclick = ()=> {
|
els.clearBtn.onclick = ()=> {
|
||||||
|
// Очищаем обычный просмотр
|
||||||
Object.values(state.open).forEach(o => {
|
Object.values(state.open).forEach(o => {
|
||||||
if (o.logEl) o.logEl.textContent = '';
|
if (o.logEl) o.logEl.textContent = '';
|
||||||
if (o.allLogs) o.allLogs = []; // Очищаем буфер логов
|
if (o.allLogs) o.allLogs = []; // Очищаем буфер логов
|
||||||
});
|
});
|
||||||
|
|
||||||
// Очищаем современный интерфейс
|
// Очищаем современный интерфейс
|
||||||
if (els.logContent) {
|
if (els.logContent) {
|
||||||
els.logContent.textContent = '';
|
els.logContent.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очищаем мультипросмотр
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||||
|
if (multiViewLog) {
|
||||||
|
multiViewLog.textContent = '';
|
||||||
|
}
|
||||||
|
// Очищаем буфер логов для мультипросмотра
|
||||||
|
const obj = state.open[containerId];
|
||||||
|
if (obj && obj.allLogs) {
|
||||||
|
obj.allLogs = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Сбрасываем счетчики
|
// Сбрасываем счетчики
|
||||||
document.querySelectorAll('.cdbg, .cinfo, .cwarn, .cerr').forEach(el => {
|
document.querySelectorAll('.cdbg, .cinfo, .cwarn, .cerr').forEach(el => {
|
||||||
el.textContent = '0';
|
el.textContent = '0';
|
||||||
@ -2225,6 +2757,10 @@ function addCounterClickHandlers() {
|
|||||||
if (state.current) {
|
if (state.current) {
|
||||||
refreshLogsAndCounters();
|
refreshLogsAndCounters();
|
||||||
}
|
}
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2237,6 +2773,10 @@ function addCounterClickHandlers() {
|
|||||||
if (state.current) {
|
if (state.current) {
|
||||||
refreshLogsAndCounters();
|
refreshLogsAndCounters();
|
||||||
}
|
}
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2249,6 +2789,10 @@ function addCounterClickHandlers() {
|
|||||||
if (state.current) {
|
if (state.current) {
|
||||||
refreshLogsAndCounters();
|
refreshLogsAndCounters();
|
||||||
}
|
}
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2261,6 +2805,10 @@ function addCounterClickHandlers() {
|
|||||||
if (state.current) {
|
if (state.current) {
|
||||||
refreshLogsAndCounters();
|
refreshLogsAndCounters();
|
||||||
}
|
}
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2482,10 +3030,7 @@ if (els.tail) {
|
|||||||
}
|
}
|
||||||
if (els.wrapToggle) {
|
if (els.wrapToggle) {
|
||||||
els.wrapToggle.onchange = ()=> {
|
els.wrapToggle.onchange = ()=> {
|
||||||
document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre');
|
applyWrapSettings();
|
||||||
if (els.logContent) {
|
|
||||||
els.logContent.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2509,6 +3054,16 @@ if (els.autoscroll) {
|
|||||||
logContent.scrollTop = logContent.scrollHeight;
|
logContent.scrollTop = logContent.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем мультипросмотр
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
state.selectedContainers.forEach(containerId => {
|
||||||
|
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||||
|
if (multiViewLog && els.autoscroll.checked) {
|
||||||
|
multiViewLog.scrollTop = multiViewLog.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2526,6 +3081,10 @@ if (els.lvlDebug) {
|
|||||||
state.levels.debug = els.lvlDebug.checked;
|
state.levels.debug = els.lvlDebug.checked;
|
||||||
updateCounterVisibility();
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (els.lvlInfo) {
|
if (els.lvlInfo) {
|
||||||
@ -2533,6 +3092,10 @@ if (els.lvlInfo) {
|
|||||||
state.levels.info = els.lvlInfo.checked;
|
state.levels.info = els.lvlInfo.checked;
|
||||||
updateCounterVisibility();
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (els.lvlWarn) {
|
if (els.lvlWarn) {
|
||||||
@ -2540,6 +3103,10 @@ if (els.lvlWarn) {
|
|||||||
state.levels.warn = els.lvlWarn.checked;
|
state.levels.warn = els.lvlWarn.checked;
|
||||||
updateCounterVisibility();
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (els.lvlErr) {
|
if (els.lvlErr) {
|
||||||
@ -2547,6 +3114,10 @@ if (els.lvlErr) {
|
|||||||
state.levels.err = els.lvlErr.checked;
|
state.levels.err = els.lvlErr.checked;
|
||||||
updateCounterVisibility();
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
|
// Обновляем multi-view если он активен
|
||||||
|
if (state.multiViewMode) {
|
||||||
|
refreshAllLogs();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2621,6 +3192,14 @@ window.addEventListener('keydown', (e)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчики для чекбоксов контейнеров
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('container-checkbox')) {
|
||||||
|
const containerId = e.target.getAttribute('data-container-id');
|
||||||
|
toggleContainerSelection(containerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user