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>
<meta charset="utf-8"/>
<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__"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
@ -110,20 +110,39 @@ a{color:var(--link)}
font-size: 12px;
}
/* Цвета для ws состояния */
#wsstate.ws-on {
color: var(--ok);
/* Кнопка состояния WebSocket */
.ws-status-btn {
background: var(--chip);
color: var(--muted);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 12px;
font-size: 11px;
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 {
color: var(--err);
font-weight: 500;
.ws-status-btn.ws-on {
background: var(--ok);
color: #0b0d12;
border-color: var(--ok);
}
#wsstate.ws-err {
color: var(--warn);
font-weight: 500;
.ws-status-btn.ws-off {
background: var(--err);
color: #0b0d12;
border-color: var(--err);
}
.ws-status-btn.ws-err {
background: var(--warn);
color: #0b0d12;
border-color: var(--warn);
}
/* Sidebar Controls */
@ -271,6 +290,22 @@ a{color:var(--link)}
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 {
display: flex;
align-items: center;
@ -279,6 +314,16 @@ a{color:var(--link)}
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"] {
margin: 0;
}
@ -320,6 +365,13 @@ a{color:var(--link)}
flex-wrap: wrap;
}
.btn-group.actions-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
width: 100%;
}
/* Main Content */
.main-content {
flex: 1;
@ -614,7 +666,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button>
</div>
</div>
<p class="subtitle">Modern Log Viewer</p>
</div>
<div class="sidebar-controls">
@ -640,7 +692,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button>
</div>
<div class="control-content" id="levels-content">
<div class="checkbox-group">
<div class="checkbox-group levels-grid">
<div class="checkbox-item">
<input type="checkbox" id="lvlDebug" checked>
<label for="lvlDebug">DEBUG</label>
@ -686,7 +738,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button>
</div>
<div class="control-content" id="options-content">
<div class="checkbox-group">
<div class="checkbox-group options-grid">
<div class="checkbox-item">
<input type="checkbox" id="autoscroll" checked>
<label for="autoscroll">Auto-scroll</label>
@ -723,7 +775,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</button>
</div>
<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="clear" class="btn"><i class="fas fa-trash"></i> Clear</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>
<input id="themeSwitch" type="checkbox" />
</div>
<span id="wsstate">ws: off</span>
<button id="wsstate" class="ws-status-btn">ws: off</button>
</div>
</div>
@ -865,25 +917,22 @@ function refreshAllLogs() {
const obj = state.open[id];
if (!obj || !obj.logEl) return;
// Получаем все строки логов из буфера
const allLines = obj.allLogs || [];
const filteredLines = [];
allLines.forEach(line => {
if (line.trim() === '') return;
// Получаем все логи из буфера
const allLogs = obj.allLogs || [];
const filteredHtml = [];
allLogs.forEach(logEntry => {
// Проверяем уровень логирования
const cls = classify(line);
if (!allowedByLevel(cls)) return;
if (!allowedByLevel(logEntry.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) {
@ -899,13 +948,14 @@ function classify(line){
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';
return '';
return 'other'; // Изменено с пустой строки на 'other'
}
function allowedByLevel(cls){
if (cls==='dbg') return state.levels.debug;
if (cls==='err') return state.levels.err;
if (cls==='warn') return state.levels.warn;
if (cls==='ok') return state.levels.info;
if (cls==='other') return true; // Всегда показываем неклассифицированные строки
return true;
}
function applyFilter(line){
@ -1223,33 +1273,34 @@ function openWs(svc, panel){
const obj = state.open[id];
if (!obj) return;
// Сохраняем все логи в буфере
// Сохраняем все логи в буфере (всегда)
if (!obj.allLogs) obj.allLogs = [];
obj.allLogs.push(html);
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;
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();
return;
}
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
if (state.current && state.current.id === id) {
if (state.current && state.current.id === id && els.logContent) {
els.logContent.innerHTML = obj.logEl.innerHTML;
const logContent = document.querySelector('.log-content');
if (els.autoscroll.checked) {
if (logContent && els.autoscroll.checked) {
logContent.scrollTop = logContent.scrollHeight;
}
}
@ -1448,6 +1499,27 @@ els.clearBtn.onclick = ()=> {
els.refreshBtn.onclick = async () => {
console.log('Refreshing services...');
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;
@ -1621,13 +1693,16 @@ els.autoscroll.onchange = ()=> {
};
els.pause.onchange = ()=> {
// При снятии паузы показываем накопленные логи
// При снятии паузы показываем накопленные логи с учетом фильтров
if (!els.pause.checked) {
Object.keys(state.open).forEach(id => {
const obj = state.open[id];
if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) {
obj.pausedBuffer.forEach(html => {
obj.logEl.insertAdjacentHTML('beforeend', html);
obj.pausedBuffer.forEach(logEntry => {
// Проверяем фильтры для каждого логированного сообщения
if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) {
obj.logEl.insertAdjacentHTML('beforeend', logEntry.html);
}
});
obj.pausedBuffer = [];