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}
-