feat: добавлены страницы ошибок и кнопка OTHER в LogLevels

- Добавлена кнопка OTHER в LogLevels для неклассифицированных логов
- Созданы красивые страницы ошибок с поддержкой темной/светлой темы
- Добавлены обработчики для ошибок 401, 403, 404, 500
- Реализована безопасность: убраны детали ошибок из пользовательского интерфейса
- Кнопка 'Войти в систему' показывается только на странице ошибки 403
- На странице 403 убран error-message, оставлен только auth-notice
- Обновлены счетчики логов для поддержки уровня OTHER
- Добавлены тестовые маршруты для проверки страниц ошибок
- Улучшен UX: адаптивный дизайн, интерактивность, навигация

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов
2025-08-17 18:49:54 +03:00
parent a979dd2838
commit d0a4b57233
5 changed files with 547 additions and 362 deletions

View File

@@ -562,6 +562,15 @@ a{color:var(--link)}
background: #c82333;
}
.other-btn {
background: #6c757d;
color: white;
}
.other-btn:hover {
background: #5a6268;
}
.btn-group {
display: flex;
gap: 8px;
@@ -1313,6 +1322,10 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
<span class="counter-label">ERROR</span>
<span class="counter-value cerr">0</span>
</button>
<button class="counter-btn other-btn" title="OTHER">
<span class="counter-label">OTHER</span>
<span class="counter-value cother">0</span>
</button>
<button id="logRefreshBtn" class="btn btn-small" title="Обновить логи и счетчики">
<i class="fas fa-sync-alt"></i> Refresh
</button>
@@ -1341,7 +1354,7 @@ const state = {
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},
levels: {debug:true, info:true, warn:true, err:true, other:true},
selectedContainers: [], // Массив ID выбранных контейнеров для мультипросмотра
multiViewMode: false, // Режим мультипросмотра
};
@@ -1365,6 +1378,7 @@ const els = {
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'),
@@ -1528,7 +1542,7 @@ function allowedByLevel(cls){
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; // Всегда показываем неклассифицированные строки
if (cls==='other') return state.levels.other; // Показываем неклассифицированные строки в зависимости от настройки
return true;
}
function applyFilter(line){
@@ -2345,7 +2359,7 @@ function openMultiViewWs(service) {
serviceName: service.service,
logEl: logEl,
wrapEl: logEl,
counters: {dbg:0, info:0, warn:0, err:0},
counters: {dbg:0, info:0, warn:0, err:0, other:0},
pausedBuffer: [],
allLogs: [] // Добавляем буфер для логов
};
@@ -2543,7 +2557,8 @@ function openWs(svc, panel){
const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo');
const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn');
const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr');
const counters = {dbg:0,info:0,warn:0,err:0};
const cother = panel.querySelector('.cother') || document.querySelector('.cother');
const counters = {dbg:0,info:0,warn:0,err:0,other:0};
const ws = new WebSocket(wsUrl(id, (svc.service||svc.name), svc.project||''));
state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: (svc.service||svc.name)};
@@ -2621,6 +2636,7 @@ function openWs(svc, panel){
cinfo.textContent = counters.info;
cwarn.textContent = counters.warn;
cerr.textContent = counters.err;
if (cother) cother.textContent = counters.other;
};
// Убираем автоматический refresh - теперь только по кнопке
@@ -3253,7 +3269,7 @@ function handleLine(id, line){
// Отладочная информация для первых нескольких строк
if (!obj.counters) {
console.error(`handleLine: Counters not initialized for container ${id}`);
obj.counters = {dbg:0, info:0, warn:0, err:0};
obj.counters = {dbg:0, info:0, warn:0, err:0, other:0};
}
// Фильтруем сообщение "Connected to container" для всех режимов
@@ -3278,6 +3294,7 @@ function handleLine(id, line){
if (cls==='ok') obj.counters.info++;
if (cls==='warn') obj.counters.warn++;
if (cls==='err') obj.counters.err++;
if (cls==='other') obj.counters.other++;
}
// Для Single View НЕ добавляем перенос строки после каждой строки лога
@@ -3643,7 +3660,8 @@ function openFanGroup(services){
const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo');
const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn');
const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr');
const counters = {dbg:0,info:0,warn:0,err:0};
const cother = panel.querySelector('.cother') || document.querySelector('.cother');
const counters = {dbg:0,info:0,warn:0,err:0,other:0};
const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||''));
state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: ('group:'+services.join(','))};
@@ -3663,6 +3681,7 @@ function openFanGroup(services){
cinfo.textContent = counters.info;
cwarn.textContent = counters.warn;
cerr.textContent = counters.err;
if (cother) cother.textContent = counters.other;
// Обновляем видимость счетчиков
updateCounterVisibility();
@@ -3707,11 +3726,13 @@ async function updateCounters(containerId) {
const cinfo = document.querySelector('.cinfo');
const cwarn = document.querySelector('.cwarn');
const cerr = document.querySelector('.cerr');
const cother = document.querySelector('.cother');
if (cdbg) cdbg.textContent = stats.debug || 0;
if (cinfo) cinfo.textContent = stats.info || 0;
if (cwarn) cwarn.textContent = stats.warn || 0;
if (cerr) cerr.textContent = stats.error || 0;
if (cother) cother.textContent = stats.other || 0;
// Обновляем видимость счетчиков
updateCounterVisibility();
@@ -3779,7 +3800,7 @@ function recalculateCounters() {
const visibleLogs = obj.allLogs.slice(-tailLines);
// Сбрасываем счетчики
obj.counters = {dbg: 0, info: 0, warn: 0, err: 0};
obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0};
// Пересчитываем счетчики только для отображаемых логов
visibleLogs.forEach(logEntry => {
@@ -3789,6 +3810,7 @@ function recalculateCounters() {
if (logEntry.cls === 'ok') obj.counters.info++;
if (logEntry.cls === 'warn') obj.counters.warn++;
if (logEntry.cls === 'err') obj.counters.err++;
if (logEntry.cls === 'other') obj.counters.other++;
}
});
@@ -3797,11 +3819,13 @@ function recalculateCounters() {
const cinfo = document.querySelector('.cinfo');
const cwarn = document.querySelector('.cwarn');
const cerr = document.querySelector('.cerr');
const cother = document.querySelector('.cother');
if (cdbg) cdbg.textContent = obj.counters.dbg;
if (cinfo) cinfo.textContent = obj.counters.info;
if (cwarn) cwarn.textContent = obj.counters.warn;
if (cerr) cerr.textContent = obj.counters.err;
if (cother) cother.textContent = obj.counters.other;
console.log(`Counters recalculated for container ${containerId} (tail: ${tailLines}):`, obj.counters);
}
@@ -3822,6 +3846,7 @@ function recalculateMultiViewCounters() {
let totalInfo = 0;
let totalWarn = 0;
let totalError = 0;
let totalOther = 0;
// Пересчитываем счетчики для каждого контейнера
for (const containerId of state.selectedContainers) {
@@ -3832,7 +3857,7 @@ function recalculateMultiViewCounters() {
const visibleLogs = obj.allLogs.slice(-tailLines);
// Сбрасываем счетчики для этого контейнера
obj.counters = {dbg: 0, info: 0, warn: 0, err: 0};
obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0};
// Пересчитываем счетчики только для отображаемых логов
visibleLogs.forEach(logEntry => {
@@ -3842,6 +3867,7 @@ function recalculateMultiViewCounters() {
if (logEntry.cls === 'ok') obj.counters.info++;
if (logEntry.cls === 'warn') obj.counters.warn++;
if (logEntry.cls === 'err') obj.counters.err++;
if (logEntry.cls === 'other') obj.counters.other++;
}
});
@@ -3850,6 +3876,7 @@ function recalculateMultiViewCounters() {
totalInfo += obj.counters.info;
totalWarn += obj.counters.warn;
totalError += obj.counters.err;
totalOther += obj.counters.other;
}
// Обновляем отображение счетчиков
@@ -3857,13 +3884,15 @@ function recalculateMultiViewCounters() {
const cinfo = document.querySelector('.cinfo');
const cwarn = document.querySelector('.cwarn');
const cerr = document.querySelector('.cerr');
const cother = document.querySelector('.cother');
if (cdbg) cdbg.textContent = totalDebug;
if (cinfo) cinfo.textContent = totalInfo;
if (cwarn) cwarn.textContent = totalWarn;
if (cerr) cerr.textContent = totalError;
if (cother) cother.textContent = totalOther;
console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError });
console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError, totalOther });
}
// Функция для обновления видимости счетчиков
@@ -3872,6 +3901,7 @@ function updateCounterVisibility() {
const infoBtn = document.querySelector('.info-btn');
const warnBtn = document.querySelector('.warn-btn');
const errorBtn = document.querySelector('.error-btn');
const otherBtn = document.querySelector('.other-btn');
if (debugBtn) {
debugBtn.classList.toggle('disabled', !state.levels.debug);
@@ -3885,6 +3915,9 @@ function updateCounterVisibility() {
if (errorBtn) {
errorBtn.classList.toggle('disabled', !state.levels.err);
}
if (otherBtn) {
otherBtn.classList.toggle('disabled', !state.levels.other);
}
}
// Функция для обновления логов и счетчиков
@@ -4020,6 +4053,7 @@ function addCounterClickHandlers() {
const infoBtn = document.querySelector('.info-btn');
const warnBtn = document.querySelector('.warn-btn');
const errorBtn = document.querySelector('.error-btn');
const otherBtn = document.querySelector('.other-btn');
if (debugBtn) {
debugBtn.onclick = () => {
@@ -4084,6 +4118,22 @@ function addCounterClickHandlers() {
}
};
}
if (otherBtn) {
otherBtn.onclick = () => {
state.levels.other = !state.levels.other;
updateCounterVisibility();
refreshAllLogs();
// Добавляем refresh для обновления логов
if (state.current) {
refreshLogsAndCounters();
}
// Обновляем multi-view если он активен
if (state.multiViewMode) {
refreshAllLogs();
}
};
}
}
@@ -4464,6 +4514,25 @@ if (els.lvlErr) {
}
};
}
if (els.lvlOther) {
els.lvlOther.onchange = ()=> {
state.levels.other = els.lvlOther.checked;
updateCounterVisibility();
refreshAllLogs();
// Обновляем multi-view если он активен
if (state.multiViewMode) {
refreshAllLogs();
setTimeout(() => {
recalculateMultiViewCounters();
}, 100);
} else {
// Пересчитываем счетчики для Single View
setTimeout(() => {
recalculateCounters();
}, 100);
}
};
}
// Hotkeys: [ ] — navigation between containers
window.addEventListener('keydown', async (e)=>{