UI improvements: removed pause from Options, updated Tail Lines gradation, renamed Snapshot to Download logs and made it full-width

This commit is contained in:
Сергей Антропов 2025-08-16 15:59:00 +03:00
parent 351b2ac041
commit 3fcaa8ad5d
2 changed files with 695 additions and 198 deletions

103
app.py
View File

@ -97,6 +97,20 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
Автор: Сергей Антропов Автор: Сергей Антропов
Сайт: https://devops.org.ru Сайт: 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 = [] items = []
try: try:
@ -140,6 +154,11 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
if container_project not in projects: if container_project not in projects:
continue continue
# Фильтрация исключенных контейнеров
if basic_info["name"] in excluded_containers:
print(f"⚠️ Пропускаем исключенный контейнер: {basic_info['name']}")
continue
# Добавляем контейнер в список # Добавляем контейнер в список
items.append(basic_info) items.append(basic_info)
@ -167,16 +186,77 @@ def load_index_html(token: str) -> str:
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def index(creds: HTTPBasicCredentials = Depends(check_basic)): def index(creds: HTTPBasicCredentials = Depends(check_basic)):
token = token_from_creds(creds) 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) @app.get("/healthz", response_class=PlainTextResponse)
def healthz(): def healthz():
return "ok" 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") @app.get("/api/projects")
def api_projects(_: HTTPBasicCredentials = Depends(check_basic)): def api_projects(_: HTTPBasicCredentials = Depends(check_basic)):
"""Получить список всех проектов Docker Compose""" """Получить список всех проектов 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") @app.get("/api/services")
def api_services(projects: Optional[str] = Query(None), include_stopped: bool = Query(False), 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: elif DEFAULT_PROJECT:
project_list = [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") @app.post("/api/snapshot")
def api_snapshot( def api_snapshot(
@ -252,24 +339,30 @@ async def ws_logs(ws: WebSocket, container_id: str, tail: int = DEFAULT_TAIL, to
# Получаем логи (только последние строки, без follow) # Получаем логи (только последние строки, без follow)
try: try:
print(f"Getting logs for container {container.name} (ID: {container.id[:12]})")
logs = container.logs(tail=tail).decode(errors="ignore") logs = container.logs(tail=tail).decode(errors="ignore")
if logs: if logs:
await ws.send_text(logs) await ws.send_text(logs)
else: else:
await ws.send_text("No logs available") await ws.send_text("No logs available")
except Exception as e: except Exception as e:
print(f"Error getting logs for {container.name}: {e}")
await ws.send_text(f"ERROR getting logs: {e}") await ws.send_text(f"ERROR getting logs: {e}")
# Простое WebSocket соединение - только отправляем логи один раз
print(f"WebSocket connection established for {container.name}")
except WebSocketDisconnect: except WebSocketDisconnect:
print("WebSocket client disconnected") print(f"WebSocket client disconnected for container {container.name}")
except Exception as e: except Exception as e:
print(f"WebSocket error: {e}") print(f"WebSocket error for {container.name}: {e}")
try: try:
await ws.send_text(f"ERROR: {e}") await ws.send_text(f"ERROR: {e}")
except: except:
pass pass
finally: finally:
try: try:
print(f"Closing WebSocket connection for container {container.name}")
await ws.close() await ws.close()
except: except:
pass pass

File diff suppressed because it is too large Load Diff