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