From 3fcaa8ad5d802462b8ae5d86d6ed705a8004716f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=90=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=BE=D0=B2?= Date: Sat, 16 Aug 2025 15:59:00 +0300 Subject: [PATCH] UI improvements: removed pause from Options, updated Tail Lines gradation, renamed Snapshot to Download logs and made it full-width --- app.py | 103 +++++- templates/index.html | 790 ++++++++++++++++++++++++++++++++----------- 2 files changed, 695 insertions(+), 198 deletions(-) diff --git a/app.py b/app.py index e5a08b4..427e808 100644 --- a/app.py +++ b/app.py @@ -97,6 +97,20 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool Автор: Сергей Антропов Сайт: https://devops.org.ru """ + # Список контейнеров, которые генерируют слишком много логов + excluded_containers = [ + "buildx_buildkit_multiarch-builder0", + "buildx_buildkit_multiarch-builder1", + "buildx_buildkit_multiarch-builder2", + "buildx_buildkit_multiarch-builder3", + "buildx_buildkit_multiarch-builder4", + "buildx_buildkit_multiarch-builder5", + "buildx_buildkit_multiarch-builder6", + "buildx_buildkit_multiarch-builder7", + "buildx_buildkit_multiarch-builder8", + "buildx_buildkit_multiarch-builder9" + ] + items = [] try: @@ -140,6 +154,11 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool if container_project not in projects: continue + # Фильтрация исключенных контейнеров + if basic_info["name"] in excluded_containers: + print(f"⚠️ Пропускаем исключенный контейнер: {basic_info['name']}") + continue + # Добавляем контейнер в список items.append(basic_info) @@ -167,16 +186,77 @@ def load_index_html(token: str) -> str: @app.get("/", response_class=HTMLResponse) def index(creds: HTTPBasicCredentials = Depends(check_basic)): token = token_from_creds(creds) - return HTMLResponse(load_index_html(token)) + return HTMLResponse( + content=load_index_html(token), + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0" + } + ) @app.get("/healthz", response_class=PlainTextResponse) def healthz(): return "ok" +@app.get("/api/logs/stats/{container_id}") +def api_logs_stats(container_id: str, _: HTTPBasicCredentials = Depends(check_basic)): + """Получить статистику логов контейнера""" + try: + # Ищем контейнер + container = None + for c in docker_client.containers.list(all=True): + if c.id.startswith(container_id): + container = c + break + + if container is None: + return JSONResponse({"error": "Container not found"}, status_code=404) + + # Получаем логи + logs = container.logs(tail=1000).decode(errors="ignore") + + # Подсчитываем статистику + stats = {"debug": 0, "info": 0, "warn": 0, "error": 0} + + for line in logs.split('\n'): + if not line.strip(): + continue + + line_lower = line.lower() + if 'level=debug' in line_lower or 'debug' in line_lower: + stats["debug"] += 1 + elif 'level=info' in line_lower or 'info' in line_lower: + stats["info"] += 1 + elif 'level=warning' in line_lower or 'level=warn' in line_lower or 'warning' in line_lower or 'warn' in line_lower: + stats["warn"] += 1 + elif 'level=error' in line_lower or 'error' in line_lower: + stats["error"] += 1 + + return JSONResponse( + content=stats, + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0" + } + ) + + except Exception as e: + print(f"Error getting log stats for {container_id}: {e}") + return JSONResponse({"error": str(e)}, status_code=500) + @app.get("/api/projects") def api_projects(_: HTTPBasicCredentials = Depends(check_basic)): """Получить список всех проектов Docker Compose""" - return JSONResponse(get_all_projects()) + return JSONResponse( + content=get_all_projects(), + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0" + } + ) @app.get("/api/services") def api_services(projects: Optional[str] = Query(None), include_stopped: bool = Query(False), @@ -193,7 +273,14 @@ def api_services(projects: Optional[str] = Query(None), include_stopped: bool = elif DEFAULT_PROJECT: project_list = [DEFAULT_PROJECT] - return JSONResponse(list_containers(projects=project_list, include_stopped=include_stopped)) + return JSONResponse( + content=list_containers(projects=project_list, include_stopped=include_stopped), + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0" + } + ) @app.post("/api/snapshot") def api_snapshot( @@ -252,24 +339,30 @@ async def ws_logs(ws: WebSocket, container_id: str, tail: int = DEFAULT_TAIL, to # Получаем логи (только последние строки, без follow) try: + print(f"Getting logs for container {container.name} (ID: {container.id[:12]})") logs = container.logs(tail=tail).decode(errors="ignore") if logs: await ws.send_text(logs) else: await ws.send_text("No logs available") except Exception as e: + print(f"Error getting logs for {container.name}: {e}") await ws.send_text(f"ERROR getting logs: {e}") + # Простое WebSocket соединение - только отправляем логи один раз + print(f"WebSocket connection established for {container.name}") + except WebSocketDisconnect: - print("WebSocket client disconnected") + print(f"WebSocket client disconnected for container {container.name}") except Exception as e: - print(f"WebSocket error: {e}") + print(f"WebSocket error for {container.name}: {e}") try: await ws.send_text(f"ERROR: {e}") except: pass finally: try: + print(f"Closing WebSocket connection for container {container.name}") await ws.close() except: pass diff --git a/templates/index.html b/templates/index.html index 8796dfe..59da2f8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -50,7 +50,7 @@ a{color:var(--link)} display: flex; align-items: center; justify-content: space-between; - margin-bottom: 8px; + margin-bottom: 0px; } .header-buttons { @@ -128,21 +128,21 @@ a{color:var(--link)} } .ws-status-btn.ws-on { - background: var(--ok); - color: #0b0d12; - border-color: var(--ok); + background: #7ea855; /* Темнее на 20% */ + color: white; + border-color: #7ea855; } .ws-status-btn.ws-off { - background: var(--err); - color: #0b0d12; - border-color: var(--err); + background: #f7768e; + color: white; + border-color: #f7768e; } .ws-status-btn.ws-err { - background: var(--warn); - color: #0b0d12; - border-color: var(--warn); + background: #e0af68; + color: white; + border-color: #e0af68; } /* Sidebar Controls */ @@ -359,6 +359,122 @@ a{color:var(--link)} background: #6b8fd8; } +.btn-small { + padding: 4px 8px; + font-size: 10px; + min-width: auto; +} + +.btn-full-width { + width: 100%; + justify-content: center; +} + +/* Стили для кнопки refresh */ +#logRefreshBtn { + background: var(--accent); + color: white; + border: none; + border-radius: 6px; + transition: all 0.2s ease; + padding: 6px 24px; /* Увеличиваем ширину в 2 раза */ + font-size: 11px; + font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + height: fit-content; +} + +#logRefreshBtn:hover { + background: var(--accent); + opacity: 0.8; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Стили для счетчиков-кнопок */ +.counter-btn { + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 6px 12px; + margin: 0 4px; + border: none; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-width: 70px; +} + +.counter-btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Убираем стиль hidden, так как счетчики больше не скрываются */ + +.counter-btn.disabled { + opacity: 0.5; + background: var(--muted) !important; + cursor: pointer; +} + +.counter-btn.disabled:hover { + opacity: 0.7; +} + +.counter-label { + font-size: 10px; + opacity: 0.9; + margin-right: 4px; +} + +.counter-value { + font-size: 12px; + font-weight: bold; +} + +/* Цвета для разных уровней логов */ +.debug-btn { + background: #6c757d; + color: white; +} + +.debug-btn:hover { + background: #5a6268; +} + +.info-btn { + background: #17a2b8; + color: white; +} + +.info-btn:hover { + background: #138496; +} + +.warn-btn { + background: #ffc107; + color: #212529; +} + +.warn-btn:hover { + background: #e0a800; +} + +.error-btn { + background: #dc3545; + color: white; +} + +.error-btn:hover { + background: #c82333; +} + .btn-group { display: flex; gap: 8px; @@ -372,6 +488,10 @@ a{color:var(--link)} width: 100%; } +.btn-group.actions-grid .btn-full-width { + grid-column: 1 / -1; +} + /* Main Content */ .main-content { flex: 1; @@ -399,12 +519,35 @@ a{color:var(--link)} } .header-badge { - background: var(--chip); color: var(--muted); - padding: 4px 8px; - border-radius: 4px; - font-size: 11px; + padding: 8px 12px; + font-size: 12px; + display: flex; + align-items: center; + gap: 8px; + height: fit-content; +} + +.header-badge select { + background: var(--chip); + color: var(--fg); border: 1px solid var(--border); + border-radius: 6px; + padding: 8px 12px; + font-size: 12px; + cursor: pointer; + outline: none; + transition: border-color 0.2s ease; + min-width: 120px; +} + +.header-badge select:focus { + border-color: var(--accent); +} + +.header-badge select option { + background: var(--bg); + color: var(--fg); } .header-controls { @@ -414,6 +557,51 @@ a{color:var(--link)} gap: 12px; } +.header-filter { + flex: 1; + min-width: 200px; + max-width: 400px; + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--fg); + font-size: 12px; + transition: border-color 0.2s ease; + margin: 0 16px; +} + +.header-filter:focus { + outline: none; + border-color: var(--accent); +} + +.header-filter::placeholder { + color: var(--muted); +} + +.header-project-select { + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--fg); + font-size: 12px; + cursor: pointer; + transition: border-color 0.2s ease; + min-width: 120px; +} + +.header-project-select:focus { + outline: none; + border-color: var(--accent); +} + +.header-project-select option { + background: var(--bg); + color: var(--fg); +} + /* Theme Toggle */ .theme-toggle { display: flex; @@ -542,7 +730,8 @@ a{color:var(--link)} border-bottom: 1px solid var(--border); display: flex; align-items: center; - gap: 12px; + justify-content: space-between; + gap: 16px; } .log-title { @@ -670,48 +859,9 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} -
- - -
-
-
- - -
-
- -
-
+
@@ -778,7 +916,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
- +
@@ -803,7 +941,12 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}

Logs

- All Projects + + + +
Theme @@ -818,10 +961,25 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}

Select a container to view logs

- DEBUG: 0 - INFO: 0 - WARN: 0 - ERROR: 0 + + + + +
@@ -856,7 +1014,7 @@ const els = { tail: document.getElementById('tail'), autoscroll: document.getElementById('autoscroll'), wrapToggle: document.getElementById('wrap'), - pause: document.getElementById('pause'), + filter: document.getElementById('filter'), wsstate: document.getElementById('wsstate'), projectBadge: document.getElementById('projectBadge'), @@ -881,6 +1039,7 @@ const els = { mobileToggle: document.getElementById('mobileToggle'), optionsBtn: document.getElementById('optionsBtn'), logoutBtn: document.getElementById('logoutBtn'), + logRefreshBtn: document.getElementById('logRefreshBtn'), }; // ----- Theme toggle ----- @@ -944,11 +1103,26 @@ function escapeHtml(s){ return s.replace(/[&<>"']/g, 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'; - return 'other'; // Изменено с пустой строки на 'other' + + // Дополнительные проверки для других форматов + 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)}..."`); + } + + return 'other'; } function allowedByLevel(cls){ if (cls==='dbg') return state.levels.debug; @@ -960,7 +1134,14 @@ function allowedByLevel(cls){ } function applyFilter(line){ if(!state.filter) return true; - try{ return new RegExp(state.filter, 'i').test(line); }catch(e){ return true; } + try{ + // Экранируем специальные символы regex для безопасного поиска + const escapedFilter = state.filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(escapedFilter, 'i').test(line); + }catch(e){ + console.error('Filter error:', e); + return true; + } } // ANSI → HTML (SGR: 0/1/3/4, 30-37) @@ -1124,7 +1305,7 @@ async function fetchProjects(){ const projects = await res.json(); console.log('Projects loaded:', projects); - // Обновляем селектор проектов + // Обновляем селектор проектов в сайдбаре const select = els.projectSelect; if (select) { select.innerHTML = ''; @@ -1140,6 +1321,28 @@ async function fetchProjects(){ select.value = localStorage.lb_project; } } + + // Обновляем селектор проектов в заголовке + const headerSelect = document.getElementById('projectSelectHeader'); + console.log('Header select element found:', !!headerSelect); + if (headerSelect) { + headerSelect.innerHTML = ''; + console.log('Adding projects to header select:', projects); + projects.forEach(project => { + const option = document.createElement('option'); + option.value = project; + option.textContent = project; + headerSelect.appendChild(option); + }); + + // Устанавливаем сохраненный проект + if (localStorage.lb_project && projects.includes(localStorage.lb_project)) { + headerSelect.value = localStorage.lb_project; + } + console.log('Header select updated, current value:', headerSelect.value); + } else { + console.error('Header select element not found!'); + } } catch (error) { console.error('Error fetching projects:', error); } @@ -1169,12 +1372,17 @@ async function fetchServices(){ state.services = data; const pj = selectedProject === 'all' ? 'all' : selectedProject; - if (els.projectBadge) { - els.projectBadge.innerHTML = 'project: '+escapeHtml(pj)+''; + // Обновляем селектор в заголовке + const headerSelect = document.getElementById('projectSelectHeader'); + if (headerSelect) { + headerSelect.value = selectedProject; } buildTabs(); if (!state.current && state.services.length) switchToSingle(state.services[0]); + + // Добавляем обработчики для счетчиков после загрузки сервисов + addCounterClickHandlers(); } catch (error) { console.error('Error fetching services:', error); } @@ -1196,6 +1404,7 @@ function wsUrl(containerId, service, project){ function closeWs(id){ const o = state.open[id]; if (!o) return; + try { o.ws.close(); } catch(e){} delete state.open[id]; } @@ -1235,10 +1444,12 @@ function openWs(svc, panel){ const id = svc.id; const logEl = panel.querySelector('.log'); const wrapEl = panel.querySelector('.logwrap'); - const cdbg = panel.querySelector('.cdbg'); - const cinfo = panel.querySelector('.cinfo'); - const cwarn = panel.querySelector('.cwarn'); - const cerr = panel.querySelector('.cerr'); + + // Ищем счетчики в panel или в глобальных элементах + const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg'); + 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 ws = new WebSocket(wsUrl(id, (svc.service||svc.name), svc.project||'')); @@ -1248,7 +1459,10 @@ function openWs(svc, panel){ ws.onclose = ()=> setWsState(Object.keys(state.open).length? 'on':'off'); ws.onerror = ()=> setWsState('err'); ws.onmessage = (ev)=>{ + console.log(`Received WebSocket message: ${ev.data.substring(0, 100)}...`); + const parts = (ev.data||'').split(/\r?\n/); + for (let i=0;i 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}`); + } + const html = `${ansiToHtml(line)}\n`; const obj = state.open[id]; if (!obj) return; @@ -1286,13 +1514,7 @@ function openWs(svc, panel){ if (!allowedByLevel(cls)) return; if (!applyFilter(line)) return; - if (els.pause.checked){ - 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) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; @@ -1356,6 +1578,9 @@ function switchToSingle(svc){ if (activeItem) { activeItem.classList.add('active'); } + + // Добавляем обработчики для счетчиков после переключения контейнера + addCounterClickHandlers(); } function openMulti(ids){ @@ -1370,6 +1595,9 @@ function openMulti(ids){ closeWs(svc.id); openWs(svc, panel); } + + // Добавляем обработчики для счетчиков после открытия мульти-контейнеров + addCounterClickHandlers(); } // ----- Copy on selection ----- @@ -1438,10 +1666,10 @@ function openFanGroup(services){ const id = fake.id; const logEl = panel.querySelector('.log'); const wrapEl = panel.querySelector('.logwrap'); - const cdbg = panel.querySelector('.cdbg'); - const cinfo = panel.querySelector('.cinfo'); - const cwarn = panel.querySelector('.cwarn'); - const cerr = panel.querySelector('.cerr'); + const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg'); + 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 ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||'')); @@ -1462,6 +1690,12 @@ function openFanGroup(services){ cinfo.textContent = counters.info; cwarn.textContent = counters.warn; cerr.textContent = counters.err; + + // Обновляем видимость счетчиков + updateCounterVisibility(); + + // Добавляем обработчики для счетчиков + addCounterClickHandlers(); }; // Show filter bar and clear previous filters @@ -1480,6 +1714,78 @@ if (els.groupBtn && els.groupBtn.onclick !== null) { }; } +// Функция для обновления счетчиков через Ajax +async function updateCounters(containerId) { + try { + const response = await fetch(`/api/logs/stats/${containerId}`); + if (response.ok) { + const stats = await response.json(); + const cdbg = document.querySelector('.cdbg'); + const cinfo = document.querySelector('.cinfo'); + const cwarn = document.querySelector('.cwarn'); + const cerr = document.querySelector('.cerr'); + + 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; + + // Обновляем видимость счетчиков + updateCounterVisibility(); + + // Добавляем обработчики для счетчиков + addCounterClickHandlers(); + } + } catch (error) { + console.error('Error updating counters:', error); + } +} + +// Функция для обновления видимости счетчиков +function updateCounterVisibility() { + const debugBtn = document.querySelector('.debug-btn'); + const infoBtn = document.querySelector('.info-btn'); + const warnBtn = document.querySelector('.warn-btn'); + const errorBtn = document.querySelector('.error-btn'); + + if (debugBtn) { + debugBtn.classList.toggle('disabled', !state.levels.debug); + } + if (infoBtn) { + infoBtn.classList.toggle('disabled', !state.levels.info); + } + if (warnBtn) { + warnBtn.classList.toggle('disabled', !state.levels.warn); + } + if (errorBtn) { + errorBtn.classList.toggle('disabled', !state.levels.err); + } +} + +// Функция для обновления логов и счетчиков +async function refreshLogsAndCounters() { + if (!state.current) { + 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 => { @@ -1521,12 +1827,94 @@ els.refreshBtn.onclick = async () => { } } }; -els.projectSelect.onchange = fetchServices; + +// Обработчик для кнопки refresh логов +els.logRefreshBtn.onclick = refreshLogsAndCounters; + +// Обработчики для счетчиков +function addCounterClickHandlers() { + const debugBtn = document.querySelector('.debug-btn'); + const infoBtn = document.querySelector('.info-btn'); + const warnBtn = document.querySelector('.warn-btn'); + const errorBtn = document.querySelector('.error-btn'); + + if (debugBtn) { + debugBtn.onclick = () => { + state.levels.debug = !state.levels.debug; + updateCounterVisibility(); + refreshAllLogs(); + // Добавляем refresh для обновления логов + if (state.current) { + refreshLogsAndCounters(); + } + }; + } + + if (infoBtn) { + infoBtn.onclick = () => { + state.levels.info = !state.levels.info; + updateCounterVisibility(); + refreshAllLogs(); + // Добавляем refresh для обновления логов + if (state.current) { + refreshLogsAndCounters(); + } + }; + } + + if (warnBtn) { + warnBtn.onclick = () => { + state.levels.warn = !state.levels.warn; + updateCounterVisibility(); + refreshAllLogs(); + // Добавляем refresh для обновления логов + if (state.current) { + refreshLogsAndCounters(); + } + }; + } + + if (errorBtn) { + errorBtn.onclick = () => { + state.levels.err = !state.levels.err; + updateCounterVisibility(); + refreshAllLogs(); + // Добавляем refresh для обновления логов + if (state.current) { + refreshLogsAndCounters(); + } + }; + } +} +if (els.projectSelect) { + els.projectSelect.onchange = fetchServices; +} + +// Функция для добавления обработчика выпадающего списка проектов в заголовке +function addHeaderProjectSelectHandler() { + const headerProjectSelect = document.getElementById('projectSelectHeader'); + console.log('Adding handler for header project select, element found:', !!headerProjectSelect); + if (headerProjectSelect) { + headerProjectSelect.onchange = () => { + console.log('Header project select changed to:', headerProjectSelect.value); + // Синхронизируем с селектором в сайдбаре + if (els.projectSelect) { + els.projectSelect.value = headerProjectSelect.value; + } + fetchServices(); + }; + } +} // Mobile menu toggle -els.mobileToggle.onclick = () => { - document.querySelector('.sidebar').classList.toggle('open'); -}; +if (els.mobileToggle) { + els.mobileToggle.onclick = () => { + const sidebar = document.querySelector('.sidebar'); + if (sidebar) { + sidebar.classList.toggle('open'); + } + }; +} // Collapsible sections document.addEventListener('DOMContentLoaded', () => { @@ -1642,104 +2030,103 @@ document.addEventListener('DOMContentLoaded', () => { }); -els.snapshotBtn.onclick = ()=>{ - if (state.current) { - sendSnapshot(state.current.id); - } else { - alert('No container selected'); - } -}; -els.tail.onchange = ()=> { - Object.keys(state.open).forEach(id=>{ - const svc = state.services.find(s=> s.id===id); - if (!svc) return; - const panel = els.grid.querySelector(`.panel[data-cid="${id}"]`); - if (!panel) return; - state.open[id].logEl.textContent=''; - closeWs(id); openWs(svc, panel); - }); - - // Обновляем современный интерфейс - if (state.current && els.logContent) { - els.logContent.textContent = 'Reconnecting...'; - } -}; -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'; - } -}; +if (els.snapshotBtn) { + els.snapshotBtn.onclick = ()=>{ + if (state.current) { + sendSnapshot(state.current.id); + } else { + alert('No container selected'); + } + }; +} +if (els.tail) { + els.tail.onchange = ()=> { + Object.keys(state.open).forEach(id=>{ + const svc = state.services.find(s=> s.id===id); + if (!svc) return; + const panel = els.grid.querySelector(`.panel[data-cid="${id}"]`); + if (!panel) return; + state.open[id].logEl.textContent=''; + closeWs(id); openWs(svc, panel); + }); + + // Обновляем современный интерфейс + if (state.current && els.logContent) { + els.logContent.textContent = 'Reconnecting...'; + } + }; +} +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'; + } + }; +} // Добавляем обработчики для autoscroll и pause -els.autoscroll.onchange = ()=> { - // Обновляем настройку автопрокрутки для всех открытых логов - Object.keys(state.open).forEach(id => { - const obj = state.open[id]; - if (obj && obj.wrapEl) { - if (els.autoscroll.checked) { - obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; - } - } - }); - - // Обновляем современный интерфейс - if (state.current && els.logContent) { - const logContent = document.querySelector('.log-content'); - if (logContent && els.autoscroll.checked) { - logContent.scrollTop = logContent.scrollHeight; - } - } -}; - -els.pause.onchange = ()=> { - // При снятии паузы показываем накопленные логи с учетом фильтров - if (!els.pause.checked) { +if (els.autoscroll) { + els.autoscroll.onchange = ()=> { + // Обновляем настройку автопрокрутки для всех открытых логов Object.keys(state.open).forEach(id => { const obj = state.open[id]; - if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) { - obj.pausedBuffer.forEach(logEntry => { - // Проверяем фильтры для каждого логированного сообщения - if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) { - obj.logEl.insertAdjacentHTML('beforeend', logEntry.html); - } - }); - obj.pausedBuffer = []; - - // Обновляем современный интерфейс - 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) { - logContent.scrollTop = logContent.scrollHeight; - } + if (obj && obj.wrapEl) { + if (els.autoscroll.checked) { + obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight; } } }); - } -}; -els.filter.oninput = ()=> { - state.filter = els.filter.value.trim(); - refreshAllLogs(); -}; -els.lvlDebug.onchange = ()=> { - state.levels.debug = els.lvlDebug.checked; - refreshAllLogs(); -}; -els.lvlInfo.onchange = ()=> { - state.levels.info = els.lvlInfo.checked; - refreshAllLogs(); -}; -els.lvlWarn.onchange = ()=> { - state.levels.warn = els.lvlWarn.checked; - refreshAllLogs(); -}; -els.lvlErr.onchange = ()=> { - state.levels.err = els.lvlErr.checked; - refreshAllLogs(); -}; + + // Обновляем современный интерфейс + if (state.current && els.logContent) { + const logContent = document.querySelector('.log-content'); + if (logContent && els.autoscroll.checked) { + logContent.scrollTop = logContent.scrollHeight; + } + } + }; +} -// Hotkeys: [ ] — tabs, M — multi + +// Обработчик для фильтра (если элемент существует) +if (els.filter) { + els.filter.oninput = ()=> { + state.filter = els.filter.value.trim(); + refreshAllLogs(); + }; +} +// Обработчики для LogLevels (если элементы существуют) +if (els.lvlDebug) { + els.lvlDebug.onchange = ()=> { + state.levels.debug = els.lvlDebug.checked; + updateCounterVisibility(); + refreshAllLogs(); + }; +} +if (els.lvlInfo) { + els.lvlInfo.onchange = ()=> { + state.levels.info = els.lvlInfo.checked; + updateCounterVisibility(); + refreshAllLogs(); + }; +} +if (els.lvlWarn) { + els.lvlWarn.onchange = ()=> { + state.levels.warn = els.lvlWarn.checked; + updateCounterVisibility(); + refreshAllLogs(); + }; +} +if (els.lvlErr) { + els.lvlErr.onchange = ()=> { + state.levels.err = els.lvlErr.checked; + updateCounterVisibility(); + refreshAllLogs(); + }; +} + +// Hotkeys: [ ] — tabs, M — multi, R — refresh window.addEventListener('keydown', (e)=>{ if (e.key==='[' || (e.ctrlKey && e.key==='ArrowLeft')){ e.preventDefault(); @@ -1756,6 +2143,10 @@ window.addEventListener('keydown', (e)=>{ const ans = prompt('IDs через запятую:\n'+list); if (ans) openMulti(ans.split(',').map(x=>x.trim()).filter(Boolean)); } + if (e.key.toLowerCase()==='r' || e.key.toLowerCase()==='к'){ + e.preventDefault(); + refreshLogsAndCounters(); + } }); // Инициализация @@ -1770,8 +2161,21 @@ window.addEventListener('keydown', (e)=>{ themeSwitch: !!els.themeSwitch }); + // Проверяем header project select + const headerSelect = document.getElementById('projectSelectHeader'); + console.log('Header project select found during init:', !!headerSelect); + await fetchProjects(); await fetchServices(); + + // Инициализируем видимость счетчиков + updateCounterVisibility(); + + // Добавляем обработчики для счетчиков + addCounterClickHandlers(); + + // Добавляем обработчик для выпадающего списка проектов в заголовке + addHeaderProjectSelectHandler(); })();