feat: улучшения интерфейса и исправления

- Исправлена фильтрация логов по уровням (INFO, DEBUG, WARN, ERROR)
- Добавлено отображение log levels в две строки по два элемента
- Добавлено отображение Options в две строки по два элемента
- Добавлено отображение Actions в две строки по две кнопки
- Исправлена кнопка Refresh - теперь перезапускает WebSocket соединение
- Изменен индикатор WebSocket состояния на кнопку с подсветкой фона
- Убрана надпись 'Modern Log Viewer' из заголовка и интерфейса
- Улучшена логика фильтрации в реальном времени
- Автор: Сергей Антропов (https://devops.org.ru)
This commit is contained in:
Сергей Антропов 2025-08-16 14:23:23 +03:00
parent 36eef6a08d
commit 351b2ac041

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/> <meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>LogBoard+ — Modern Log Viewer</title> <title>LogBoard+</title>
<meta name="x-token" content="__TOKEN__"/> <meta name="x-token" content="__TOKEN__"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style> <style>
@ -110,20 +110,39 @@ a{color:var(--link)}
font-size: 12px; font-size: 12px;
} }
/* Цвета для ws состояния */ /* Кнопка состояния WebSocket */
#wsstate.ws-on { .ws-status-btn {
color: var(--ok); background: var(--chip);
color: var(--muted);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 12px;
font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: default;
transition: all 0.3s ease;
font-family: inherit;
min-width: 60px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
#wsstate.ws-off { .ws-status-btn.ws-on {
color: var(--err); background: var(--ok);
font-weight: 500; color: #0b0d12;
border-color: var(--ok);
} }
#wsstate.ws-err { .ws-status-btn.ws-off {
color: var(--warn); background: var(--err);
font-weight: 500; color: #0b0d12;
border-color: var(--err);
}
.ws-status-btn.ws-err {
background: var(--warn);
color: #0b0d12;
border-color: var(--warn);
} }
/* Sidebar Controls */ /* Sidebar Controls */
@ -271,6 +290,22 @@ a{color:var(--link)}
gap: 8px; gap: 8px;
} }
.checkbox-group.levels-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
width: 100%;
align-items: center;
}
.checkbox-group.options-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
width: 100%;
align-items: center;
}
.checkbox-item { .checkbox-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -279,6 +314,16 @@ a{color:var(--link)}
color: var(--muted); color: var(--muted);
} }
.levels-grid .checkbox-item {
min-height: 20px;
justify-content: flex-start;
}
.options-grid .checkbox-item {
min-height: 20px;
justify-content: flex-start;
}
.checkbox-item input[type="checkbox"] { .checkbox-item input[type="checkbox"] {
margin: 0; margin: 0;
} }
@ -320,6 +365,13 @@ a{color:var(--link)}
flex-wrap: wrap; flex-wrap: wrap;
} }
.btn-group.actions-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
width: 100%;
}
/* Main Content */ /* Main Content */
.main-content { .main-content {
flex: 1; flex: 1;
@ -614,7 +666,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button> </button>
</div> </div>
</div> </div>
<p class="subtitle">Modern Log Viewer</p>
</div> </div>
<div class="sidebar-controls"> <div class="sidebar-controls">
@ -640,7 +692,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button> </button>
</div> </div>
<div class="control-content" id="levels-content"> <div class="control-content" id="levels-content">
<div class="checkbox-group"> <div class="checkbox-group levels-grid">
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="lvlDebug" checked> <input type="checkbox" id="lvlDebug" checked>
<label for="lvlDebug">DEBUG</label> <label for="lvlDebug">DEBUG</label>
@ -686,7 +738,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button> </button>
</div> </div>
<div class="control-content" id="options-content"> <div class="control-content" id="options-content">
<div class="checkbox-group"> <div class="checkbox-group options-grid">
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="autoscroll" checked> <input type="checkbox" id="autoscroll" checked>
<label for="autoscroll">Auto-scroll</label> <label for="autoscroll">Auto-scroll</label>
@ -723,7 +775,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button> </button>
</div> </div>
<div class="control-content" id="actions-content"> <div class="control-content" id="actions-content">
<div class="btn-group"> <div class="btn-group actions-grid">
<button id="refresh" class="btn"><i class="fas fa-sync-alt"></i> Refresh</button> <button id="refresh" class="btn"><i class="fas fa-sync-alt"></i> Refresh</button>
<button id="clear" class="btn"><i class="fas fa-trash"></i> Clear</button> <button id="clear" class="btn"><i class="fas fa-trash"></i> Clear</button>
<button id="snapshot" class="btn"><i class="fas fa-download"></i> Snapshot</button> <button id="snapshot" class="btn"><i class="fas fa-download"></i> Snapshot</button>
@ -757,7 +809,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
<span>Theme</span> <span>Theme</span>
<input id="themeSwitch" type="checkbox" /> <input id="themeSwitch" type="checkbox" />
</div> </div>
<span id="wsstate">ws: off</span> <button id="wsstate" class="ws-status-btn">ws: off</button>
</div> </div>
</div> </div>
@ -865,25 +917,22 @@ function refreshAllLogs() {
const obj = state.open[id]; const obj = state.open[id];
if (!obj || !obj.logEl) return; if (!obj || !obj.logEl) return;
// Получаем все строки логов из буфера // Получаем все логи из буфера
const allLines = obj.allLogs || []; const allLogs = obj.allLogs || [];
const filteredLines = []; const filteredHtml = [];
allLines.forEach(line => { allLogs.forEach(logEntry => {
if (line.trim() === '') return;
// Проверяем уровень логирования // Проверяем уровень логирования
const cls = classify(line); if (!allowedByLevel(logEntry.cls)) return;
if (!allowedByLevel(cls)) return;
// Проверяем фильтр // Проверяем фильтр
if (!applyFilter(line)) return; if (!applyFilter(logEntry.line)) return;
filteredLines.push(line); filteredHtml.push(logEntry.html);
}); });
// Обновляем отображение // Обновляем отображение
obj.logEl.innerHTML = filteredLines.join('\n'); obj.logEl.innerHTML = filteredHtml.join('');
// Обновляем современный интерфейс // Обновляем современный интерфейс
if (state.current && state.current.id === id && els.logContent) { if (state.current && state.current.id === id && els.logContent) {
@ -899,13 +948,14 @@ function classify(line){
if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err'; if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn'; 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 (/\b(info|started|listening|ready|up)\b|level=info/.test(l)) return 'ok';
return ''; return 'other'; // Изменено с пустой строки на 'other'
} }
function allowedByLevel(cls){ function allowedByLevel(cls){
if (cls==='dbg') return state.levels.debug; if (cls==='dbg') return state.levels.debug;
if (cls==='err') return state.levels.err; if (cls==='err') return state.levels.err;
if (cls==='warn') return state.levels.warn; if (cls==='warn') return state.levels.warn;
if (cls==='ok') return state.levels.info; if (cls==='ok') return state.levels.info;
if (cls==='other') return true; // Всегда показываем неклассифицированные строки
return true; return true;
} }
function applyFilter(line){ function applyFilter(line){
@ -1223,33 +1273,34 @@ function openWs(svc, panel){
const obj = state.open[id]; const obj = state.open[id];
if (!obj) return; if (!obj) return;
// Сохраняем все логи в буфере // Сохраняем все логи в буфере (всегда)
if (!obj.allLogs) obj.allLogs = []; if (!obj.allLogs) obj.allLogs = [];
obj.allLogs.push(html); 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; if (!allowedByLevel(cls)) return;
if (!applyFilter(line)) return; if (!applyFilter(line)) return;
if (els.pause.checked){ if (els.pause.checked){
obj.pausedBuffer.push(html); if (!obj.pausedBuffer) obj.pausedBuffer = [];
obj.pausedBuffer.push({html: html, line: line, cls: cls});
if (obj.pausedBuffer.length>5000) obj.pausedBuffer.shift(); if (obj.pausedBuffer.length>5000) obj.pausedBuffer.shift();
return; return;
} }
obj.logEl.insertAdjacentHTML('beforeend', html); obj.logEl.insertAdjacentHTML('beforeend', html);
if (els.autoscroll.checked) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
// Update modern interface // Update modern interface
if (state.current && state.current.id === id) { 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 (els.autoscroll.checked) { if (logContent && els.autoscroll.checked) {
logContent.scrollTop = logContent.scrollHeight; logContent.scrollTop = logContent.scrollHeight;
} }
} }
@ -1448,6 +1499,27 @@ els.clearBtn.onclick = ()=> {
els.refreshBtn.onclick = async () => { els.refreshBtn.onclick = async () => {
console.log('Refreshing services...'); console.log('Refreshing services...');
await fetchServices(); await fetchServices();
// Если есть текущий контейнер, перезапускаем его WebSocket соединение
if (state.current) {
console.log('Reconnecting to current container:', state.current.id);
const currentId = state.current.id;
// Закрываем текущее соединение
closeWs(currentId);
// Находим обновленный контейнер в списке
const updatedContainer = state.services.find(s => s.id === currentId);
if (updatedContainer) {
// Переключаемся на обновленный контейнер
switchToSingle(updatedContainer);
} else {
// Если контейнер больше не существует, переключаемся на первый доступный
if (state.services.length > 0) {
switchToSingle(state.services[0]);
}
}
}
}; };
els.projectSelect.onchange = fetchServices; els.projectSelect.onchange = fetchServices;
@ -1621,13 +1693,16 @@ els.autoscroll.onchange = ()=> {
}; };
els.pause.onchange = ()=> { els.pause.onchange = ()=> {
// При снятии паузы показываем накопленные логи // При снятии паузы показываем накопленные логи с учетом фильтров
if (!els.pause.checked) { if (!els.pause.checked) {
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.pausedBuffer && obj.pausedBuffer.length > 0) { if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) {
obj.pausedBuffer.forEach(html => { obj.pausedBuffer.forEach(logEntry => {
obj.logEl.insertAdjacentHTML('beforeend', html); // Проверяем фильтры для каждого логированного сообщения
if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) {
obj.logEl.insertAdjacentHTML('beforeend', logEntry.html);
}
}); });
obj.pausedBuffer = []; obj.pausedBuffer = [];