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:
259
templates/error.html
Normal file
259
templates/error.html
Normal file
@@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ error_title }} - LogBoard+</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #1a1b26;
|
||||
--fg: #c0caf5;
|
||||
--panel: #24283b;
|
||||
--border: #414868;
|
||||
--accent: #7aa2f7;
|
||||
--muted: #565a6e;
|
||||
--ok: #9ece6a;
|
||||
--warn: #e0af68;
|
||||
--err: #f7768e;
|
||||
--chip: #414868;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #d5d6db;
|
||||
--fg: #343b58;
|
||||
--panel: #e1e2e7;
|
||||
--border: #9699a3;
|
||||
--accent: #34548a;
|
||||
--muted: #9699a3;
|
||||
--ok: #485e30;
|
||||
--warn: #8f5e15;
|
||||
--err: #8c4351;
|
||||
--chip: #d5d6db;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
max-width: 600px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--accent);
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--chip);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.auth-notice {
|
||||
background: var(--warn);
|
||||
color: var(--bg);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-icon">
|
||||
{% if error_code == 401 %}
|
||||
🔐
|
||||
{% elif error_code == 403 %}
|
||||
🚫
|
||||
{% elif error_code == 404 %}
|
||||
🔍
|
||||
{% elif error_code == 500 %}
|
||||
⚠️
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="error-code">{{ error_code }}</div>
|
||||
<div class="error-title">{{ error_title }}</div>
|
||||
|
||||
{% if error_code == 401 %}
|
||||
<div class="auth-notice">
|
||||
🔐 Эта страница требует авторизации
|
||||
</div>
|
||||
{% elif error_code == 403 %}
|
||||
<div class="auth-notice">
|
||||
🚫 У вас нет прав для доступа к этой странице
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error_code != 403 %}
|
||||
<div class="error-message">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="/" class="btn">На главную</a>
|
||||
{% if error_code == 403 %}
|
||||
<a href="/login" class="btn btn-secondary">Войти в систему</a>
|
||||
{% endif %}
|
||||
<button onclick="history.back()" class="btn btn-secondary">Назад</button>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
LogBoard+ - Веб-панель для просмотра логов микросервисов<br>
|
||||
Автор: <a href="https://devops.org.ru" target="_blank">Сергей Антропов</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Автоматическое определение темы
|
||||
const savedTheme = localStorage.getItem('lb_theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Переключатель темы
|
||||
function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('lb_theme', newTheme);
|
||||
}
|
||||
|
||||
// Добавляем обработчик клавиш для переключения темы (Ctrl+T)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 't') {
|
||||
e.preventDefault();
|
||||
toggleTheme();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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)=>{
|
||||
|
||||
Reference in New Issue
Block a user