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
"""
# Список контейнеров, которые генерируют слишком много логов
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

File diff suppressed because it is too large Load Diff