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:
parent
351b2ac041
commit
3fcaa8ad5d
103
app.py
103
app.py
@ -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
|
||||||
|
@ -50,7 +50,7 @@ a{color:var(--link)}
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buttons {
|
.header-buttons {
|
||||||
@ -128,21 +128,21 @@ a{color:var(--link)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ws-status-btn.ws-on {
|
.ws-status-btn.ws-on {
|
||||||
background: var(--ok);
|
background: #7ea855; /* Темнее на 20% */
|
||||||
color: #0b0d12;
|
color: white;
|
||||||
border-color: var(--ok);
|
border-color: #7ea855;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-status-btn.ws-off {
|
.ws-status-btn.ws-off {
|
||||||
background: var(--err);
|
background: #f7768e;
|
||||||
color: #0b0d12;
|
color: white;
|
||||||
border-color: var(--err);
|
border-color: #f7768e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ws-status-btn.ws-err {
|
.ws-status-btn.ws-err {
|
||||||
background: var(--warn);
|
background: #e0af68;
|
||||||
color: #0b0d12;
|
color: white;
|
||||||
border-color: var(--warn);
|
border-color: #e0af68;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Controls */
|
/* Sidebar Controls */
|
||||||
@ -359,6 +359,122 @@ a{color:var(--link)}
|
|||||||
background: #6b8fd8;
|
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 {
|
.btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -372,6 +488,10 @@ a{color:var(--link)}
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group.actions-grid .btn-full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Content */
|
/* Main Content */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -399,12 +519,35 @@ a{color:var(--link)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-badge {
|
.header-badge {
|
||||||
background: var(--chip);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 4px 8px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
font-size: 12px;
|
||||||
font-size: 11px;
|
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: 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 {
|
.header-controls {
|
||||||
@ -414,6 +557,51 @@ a{color:var(--link)}
|
|||||||
gap: 12px;
|
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 */
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -542,7 +730,8 @@ a{color:var(--link)}
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-title {
|
.log-title {
|
||||||
@ -670,48 +859,9 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-controls">
|
<div class="sidebar-controls">
|
||||||
<div class="control-group collapsible" data-section="project">
|
|
||||||
<div class="control-header">
|
|
||||||
<label>Project</label>
|
|
||||||
<button class="collapse-btn" data-target="project">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-content" id="project-content">
|
|
||||||
<select id="projectSelect">
|
|
||||||
<option value="all">All Projects</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group collapsible" data-section="levels">
|
|
||||||
<div class="control-header">
|
|
||||||
<label>Log Levels</label>
|
|
||||||
<button class="collapse-btn" data-target="levels">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-content" id="levels-content">
|
|
||||||
<div class="checkbox-group levels-grid">
|
|
||||||
<div class="checkbox-item">
|
|
||||||
<input type="checkbox" id="lvlDebug" checked>
|
|
||||||
<label for="lvlDebug">DEBUG</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-item">
|
|
||||||
<input type="checkbox" id="lvlInfo" checked>
|
|
||||||
<label for="lvlInfo">INFO</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-item">
|
|
||||||
<input type="checkbox" id="lvlWarn" checked>
|
|
||||||
<label for="lvlWarn">WARN</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-item">
|
|
||||||
<input type="checkbox" id="lvlErr" checked>
|
|
||||||
<label for="lvlErr">ERROR</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group collapsible" data-section="tail">
|
<div class="control-group collapsible" data-section="tail">
|
||||||
<div class="control-header">
|
<div class="control-header">
|
||||||
@ -722,10 +872,12 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</div>
|
</div>
|
||||||
<div class="control-content" id="tail-content">
|
<div class="control-content" id="tail-content">
|
||||||
<select id="tail">
|
<select id="tail">
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
<option value="200">200</option>
|
<option value="200">200</option>
|
||||||
|
<option value="300">300</option>
|
||||||
<option value="500" selected>500</option>
|
<option value="500" selected>500</option>
|
||||||
<option value="1000">1000</option>
|
<option value="1000">1000</option>
|
||||||
<option value="0">All</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -747,25 +899,11 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
<input type="checkbox" id="wrap" checked>
|
<input type="checkbox" id="wrap" checked>
|
||||||
<label for="wrap">Wrap text</label>
|
<label for="wrap">Wrap text</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox-item">
|
|
||||||
<input type="checkbox" id="pause">
|
|
||||||
<label for="pause">Pause</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group collapsible" data-section="filter">
|
|
||||||
<div class="control-header">
|
|
||||||
<label>Filter</label>
|
|
||||||
<button class="collapse-btn" data-target="filter">
|
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-content" id="filter-content">
|
|
||||||
<input id="filter" type="text" placeholder="Filter logs (regex)…"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group collapsible" data-section="actions">
|
<div class="control-group collapsible" data-section="actions">
|
||||||
<div class="control-header">
|
<div class="control-header">
|
||||||
@ -778,7 +916,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
<div class="btn-group actions-grid">
|
<div class="btn-group actions-grid">
|
||||||
<button id="refresh" class="btn"><i class="fas fa-sync-alt"></i> Refresh</button>
|
<button id="refresh" class="btn"><i class="fas fa-sync-alt"></i> Refresh</button>
|
||||||
<button id="clear" class="btn"><i class="fas fa-trash"></i> Clear</button>
|
<button id="clear" class="btn"><i class="fas fa-trash"></i> Clear</button>
|
||||||
<button id="snapshot" class="btn"><i class="fas fa-download"></i> Snapshot</button>
|
<button id="snapshot" class="btn btn-full-width"><i class="fas fa-download"></i> Download logs</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -803,7 +941,12 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="header-title">Logs</h2>
|
<h2 class="header-title">Logs</h2>
|
||||||
<span class="header-badge" id="projectBadge">All Projects</span>
|
<span class="header-badge" id="projectBadge">
|
||||||
|
<select id="projectSelectHeader">
|
||||||
|
<option value="all">All Projects</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<input id="filter" type="text" placeholder="Filter logs (regex)…" class="header-filter"/>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<div class="theme-toggle">
|
<div class="theme-toggle">
|
||||||
<span>Theme</span>
|
<span>Theme</span>
|
||||||
@ -818,10 +961,25 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
<div class="log-header">
|
<div class="log-header">
|
||||||
<h3 class="log-title" id="logTitle">Select a container to view logs</h3>
|
<h3 class="log-title" id="logTitle">Select a container to view logs</h3>
|
||||||
<div class="log-controls">
|
<div class="log-controls">
|
||||||
<span class="counter">DEBUG: <span class="cdbg">0</span></span>
|
<button class="counter-btn debug-btn" title="DEBUG">
|
||||||
<span class="counter">INFO: <span class="cinfo">0</span></span>
|
<span class="counter-label">DEBUG</span>
|
||||||
<span class="counter">WARN: <span class="cwarn">0</span></span>
|
<span class="counter-value cdbg">0</span>
|
||||||
<span class="counter">ERROR: <span class="cerr">0</span></span>
|
</button>
|
||||||
|
<button class="counter-btn info-btn" title="INFO">
|
||||||
|
<span class="counter-label">INFO</span>
|
||||||
|
<span class="counter-value cinfo">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="counter-btn warn-btn" title="WARN">
|
||||||
|
<span class="counter-label">WARN</span>
|
||||||
|
<span class="counter-value cwarn">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="counter-btn error-btn" title="ERROR">
|
||||||
|
<span class="counter-label">ERROR</span>
|
||||||
|
<span class="counter-value cerr">0</span>
|
||||||
|
</button>
|
||||||
|
<button id="logRefreshBtn" class="btn btn-small" title="Обновить логи и счетчики">
|
||||||
|
<i class="fas fa-sync-alt"></i> Refresh
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-content">
|
<div class="log-content">
|
||||||
@ -856,7 +1014,7 @@ const els = {
|
|||||||
tail: document.getElementById('tail'),
|
tail: document.getElementById('tail'),
|
||||||
autoscroll: document.getElementById('autoscroll'),
|
autoscroll: document.getElementById('autoscroll'),
|
||||||
wrapToggle: document.getElementById('wrap'),
|
wrapToggle: document.getElementById('wrap'),
|
||||||
pause: document.getElementById('pause'),
|
|
||||||
filter: document.getElementById('filter'),
|
filter: document.getElementById('filter'),
|
||||||
wsstate: document.getElementById('wsstate'),
|
wsstate: document.getElementById('wsstate'),
|
||||||
projectBadge: document.getElementById('projectBadge'),
|
projectBadge: document.getElementById('projectBadge'),
|
||||||
@ -881,6 +1039,7 @@ const els = {
|
|||||||
mobileToggle: document.getElementById('mobileToggle'),
|
mobileToggle: document.getElementById('mobileToggle'),
|
||||||
optionsBtn: document.getElementById('optionsBtn'),
|
optionsBtn: document.getElementById('optionsBtn'),
|
||||||
logoutBtn: document.getElementById('logoutBtn'),
|
logoutBtn: document.getElementById('logoutBtn'),
|
||||||
|
logRefreshBtn: document.getElementById('logRefreshBtn'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----- Theme toggle -----
|
// ----- Theme toggle -----
|
||||||
@ -944,11 +1103,26 @@ function escapeHtml(s){ return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<
|
|||||||
|
|
||||||
function classify(line){
|
function classify(line){
|
||||||
const l = line.toLowerCase();
|
const l = line.toLowerCase();
|
||||||
|
|
||||||
|
// Проверяем различные форматы уровней логирования
|
||||||
if (/\bdebug\b|level=debug| \[debug\]/.test(l)) return 'dbg';
|
if (/\bdebug\b|level=debug| \[debug\]/.test(l)) return 'dbg';
|
||||||
if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
|
if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
|
||||||
if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn';
|
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';
|
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){
|
function allowedByLevel(cls){
|
||||||
if (cls==='dbg') return state.levels.debug;
|
if (cls==='dbg') return state.levels.debug;
|
||||||
@ -960,7 +1134,14 @@ function allowedByLevel(cls){
|
|||||||
}
|
}
|
||||||
function applyFilter(line){
|
function applyFilter(line){
|
||||||
if(!state.filter) return true;
|
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)
|
// ANSI → HTML (SGR: 0/1/3/4, 30-37)
|
||||||
@ -1124,7 +1305,7 @@ async function fetchProjects(){
|
|||||||
const projects = await res.json();
|
const projects = await res.json();
|
||||||
console.log('Projects loaded:', projects);
|
console.log('Projects loaded:', projects);
|
||||||
|
|
||||||
// Обновляем селектор проектов
|
// Обновляем селектор проектов в сайдбаре
|
||||||
const select = els.projectSelect;
|
const select = els.projectSelect;
|
||||||
if (select) {
|
if (select) {
|
||||||
select.innerHTML = '<option value="all">All Projects</option>';
|
select.innerHTML = '<option value="all">All Projects</option>';
|
||||||
@ -1140,6 +1321,28 @@ async function fetchProjects(){
|
|||||||
select.value = localStorage.lb_project;
|
select.value = localStorage.lb_project;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем селектор проектов в заголовке
|
||||||
|
const headerSelect = document.getElementById('projectSelectHeader');
|
||||||
|
console.log('Header select element found:', !!headerSelect);
|
||||||
|
if (headerSelect) {
|
||||||
|
headerSelect.innerHTML = '<option value="all">All Projects</option>';
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching projects:', error);
|
console.error('Error fetching projects:', error);
|
||||||
}
|
}
|
||||||
@ -1169,12 +1372,17 @@ async function fetchServices(){
|
|||||||
state.services = data;
|
state.services = data;
|
||||||
const pj = selectedProject === 'all' ? 'all' : selectedProject;
|
const pj = selectedProject === 'all' ? 'all' : selectedProject;
|
||||||
|
|
||||||
if (els.projectBadge) {
|
// Обновляем селектор в заголовке
|
||||||
els.projectBadge.innerHTML = 'project: <em>'+escapeHtml(pj)+'</em>';
|
const headerSelect = document.getElementById('projectSelectHeader');
|
||||||
|
if (headerSelect) {
|
||||||
|
headerSelect.value = selectedProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTabs();
|
buildTabs();
|
||||||
if (!state.current && state.services.length) switchToSingle(state.services[0]);
|
if (!state.current && state.services.length) switchToSingle(state.services[0]);
|
||||||
|
|
||||||
|
// Добавляем обработчики для счетчиков после загрузки сервисов
|
||||||
|
addCounterClickHandlers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching services:', error);
|
console.error('Error fetching services:', error);
|
||||||
}
|
}
|
||||||
@ -1196,6 +1404,7 @@ function wsUrl(containerId, service, project){
|
|||||||
function closeWs(id){
|
function closeWs(id){
|
||||||
const o = state.open[id];
|
const o = state.open[id];
|
||||||
if (!o) return;
|
if (!o) return;
|
||||||
|
|
||||||
try { o.ws.close(); } catch(e){}
|
try { o.ws.close(); } catch(e){}
|
||||||
delete state.open[id];
|
delete state.open[id];
|
||||||
}
|
}
|
||||||
@ -1235,10 +1444,12 @@ function openWs(svc, panel){
|
|||||||
const id = svc.id;
|
const id = svc.id;
|
||||||
const logEl = panel.querySelector('.log');
|
const logEl = panel.querySelector('.log');
|
||||||
const wrapEl = panel.querySelector('.logwrap');
|
const wrapEl = panel.querySelector('.logwrap');
|
||||||
const cdbg = panel.querySelector('.cdbg');
|
|
||||||
const cinfo = panel.querySelector('.cinfo');
|
// Ищем счетчики в panel или в глобальных элементах
|
||||||
const cwarn = panel.querySelector('.cwarn');
|
const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg');
|
||||||
const cerr = panel.querySelector('.cerr');
|
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 counters = {dbg:0,info:0,warn:0,err:0};
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl(id, (svc.service||svc.name), svc.project||''));
|
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.onclose = ()=> setWsState(Object.keys(state.open).length? 'on':'off');
|
||||||
ws.onerror = ()=> setWsState('err');
|
ws.onerror = ()=> setWsState('err');
|
||||||
ws.onmessage = (ev)=>{
|
ws.onmessage = (ev)=>{
|
||||||
|
console.log(`Received WebSocket message: ${ev.data.substring(0, 100)}...`);
|
||||||
|
|
||||||
const parts = (ev.data||'').split(/\r?\n/);
|
const parts = (ev.data||'').split(/\r?\n/);
|
||||||
|
|
||||||
for (let i=0;i<parts.length;i++){
|
for (let i=0;i<parts.length;i++){
|
||||||
if (parts[i].length===0 && i===parts.length-1) continue;
|
if (parts[i].length===0 && i===parts.length-1) continue;
|
||||||
// harvest instance ids if present
|
// harvest instance ids if present
|
||||||
@ -1256,12 +1470,16 @@ function openWs(svc, panel){
|
|||||||
if (pr){ if (!(pr.id8 in inst.filters)) { inst.filters[pr.id8] = true; updateIdFiltersBar(); } }
|
if (pr){ if (!(pr.id8 in inst.filters)) { inst.filters[pr.id8] = true; updateIdFiltersBar(); } }
|
||||||
handleLine(id, parts[i]);
|
handleLine(id, parts[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем счетчики после обработки всех строк
|
||||||
cdbg.textContent = counters.dbg;
|
cdbg.textContent = counters.dbg;
|
||||||
cinfo.textContent = counters.info;
|
cinfo.textContent = counters.info;
|
||||||
cwarn.textContent = counters.warn;
|
cwarn.textContent = counters.warn;
|
||||||
cerr.textContent = counters.err;
|
cerr.textContent = counters.err;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Убираем автоматический refresh - теперь только по кнопке
|
||||||
|
|
||||||
function handleLine(id, line){
|
function handleLine(id, line){
|
||||||
const cls = classify(line);
|
const cls = classify(line);
|
||||||
if (cls==='dbg') counters.dbg++;
|
if (cls==='dbg') counters.dbg++;
|
||||||
@ -1269,6 +1487,16 @@ function openWs(svc, panel){
|
|||||||
if (cls==='warn') counters.warn++;
|
if (cls==='warn') counters.warn++;
|
||||||
if (cls==='err') counters.err++;
|
if (cls==='err') counters.err++;
|
||||||
|
|
||||||
|
// Отладочная информация для первых нескольких строк
|
||||||
|
if (counters.dbg + counters.info + counters.warn + counters.err < 10) {
|
||||||
|
console.log(`Line: "${line.substring(0, 100)}..." -> 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 = `<span class="line ${cls}">${ansiToHtml(line)}</span>\n`;
|
const html = `<span class="line ${cls}">${ansiToHtml(line)}</span>\n`;
|
||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
@ -1286,13 +1514,7 @@ function openWs(svc, panel){
|
|||||||
if (!allowedByLevel(cls)) return;
|
if (!allowedByLevel(cls)) return;
|
||||||
if (!applyFilter(line)) 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);
|
obj.logEl.insertAdjacentHTML('beforeend', html);
|
||||||
if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
||||||
|
|
||||||
@ -1356,6 +1578,9 @@ function switchToSingle(svc){
|
|||||||
if (activeItem) {
|
if (activeItem) {
|
||||||
activeItem.classList.add('active');
|
activeItem.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчики для счетчиков после переключения контейнера
|
||||||
|
addCounterClickHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMulti(ids){
|
function openMulti(ids){
|
||||||
@ -1370,6 +1595,9 @@ function openMulti(ids){
|
|||||||
closeWs(svc.id);
|
closeWs(svc.id);
|
||||||
openWs(svc, panel);
|
openWs(svc, panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчики для счетчиков после открытия мульти-контейнеров
|
||||||
|
addCounterClickHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Copy on selection -----
|
// ----- Copy on selection -----
|
||||||
@ -1438,10 +1666,10 @@ function openFanGroup(services){
|
|||||||
const id = fake.id;
|
const id = fake.id;
|
||||||
const logEl = panel.querySelector('.log');
|
const logEl = panel.querySelector('.log');
|
||||||
const wrapEl = panel.querySelector('.logwrap');
|
const wrapEl = panel.querySelector('.logwrap');
|
||||||
const cdbg = panel.querySelector('.cdbg');
|
const cdbg = panel.querySelector('.cdbg') || document.querySelector('.cdbg');
|
||||||
const cinfo = panel.querySelector('.cinfo');
|
const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo');
|
||||||
const cwarn = panel.querySelector('.cwarn');
|
const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn');
|
||||||
const cerr = panel.querySelector('.cerr');
|
const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr');
|
||||||
const counters = {dbg:0,info:0,warn:0,err:0};
|
const counters = {dbg:0,info:0,warn:0,err:0};
|
||||||
|
|
||||||
const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||''));
|
const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||''));
|
||||||
@ -1462,6 +1690,12 @@ function openFanGroup(services){
|
|||||||
cinfo.textContent = counters.info;
|
cinfo.textContent = counters.info;
|
||||||
cwarn.textContent = counters.warn;
|
cwarn.textContent = counters.warn;
|
||||||
cerr.textContent = counters.err;
|
cerr.textContent = counters.err;
|
||||||
|
|
||||||
|
// Обновляем видимость счетчиков
|
||||||
|
updateCounterVisibility();
|
||||||
|
|
||||||
|
// Добавляем обработчики для счетчиков
|
||||||
|
addCounterClickHandlers();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show filter bar and clear previous filters
|
// 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
|
// Controls
|
||||||
els.clearBtn.onclick = ()=> {
|
els.clearBtn.onclick = ()=> {
|
||||||
Object.values(state.open).forEach(o => {
|
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
|
// Mobile menu toggle
|
||||||
els.mobileToggle.onclick = () => {
|
if (els.mobileToggle) {
|
||||||
document.querySelector('.sidebar').classList.toggle('open');
|
els.mobileToggle.onclick = () => {
|
||||||
};
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.toggle('open');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Collapsible sections
|
// Collapsible sections
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -1642,14 +2030,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
els.snapshotBtn.onclick = ()=>{
|
if (els.snapshotBtn) {
|
||||||
|
els.snapshotBtn.onclick = ()=>{
|
||||||
if (state.current) {
|
if (state.current) {
|
||||||
sendSnapshot(state.current.id);
|
sendSnapshot(state.current.id);
|
||||||
} else {
|
} else {
|
||||||
alert('No container selected');
|
alert('No container selected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
els.tail.onchange = ()=> {
|
}
|
||||||
|
if (els.tail) {
|
||||||
|
els.tail.onchange = ()=> {
|
||||||
Object.keys(state.open).forEach(id=>{
|
Object.keys(state.open).forEach(id=>{
|
||||||
const svc = state.services.find(s=> s.id===id);
|
const svc = state.services.find(s=> s.id===id);
|
||||||
if (!svc) return;
|
if (!svc) return;
|
||||||
@ -1663,16 +2054,20 @@ els.tail.onchange = ()=> {
|
|||||||
if (state.current && els.logContent) {
|
if (state.current && els.logContent) {
|
||||||
els.logContent.textContent = 'Reconnecting...';
|
els.logContent.textContent = 'Reconnecting...';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
els.wrapToggle.onchange = ()=> {
|
}
|
||||||
|
if (els.wrapToggle) {
|
||||||
|
els.wrapToggle.onchange = ()=> {
|
||||||
document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre');
|
document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre');
|
||||||
if (els.logContent) {
|
if (els.logContent) {
|
||||||
els.logContent.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre';
|
els.logContent.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем обработчики для autoscroll и pause
|
// Добавляем обработчики для autoscroll и pause
|
||||||
els.autoscroll.onchange = ()=> {
|
if (els.autoscroll) {
|
||||||
|
els.autoscroll.onchange = ()=> {
|
||||||
// Обновляем настройку автопрокрутки для всех открытых логов
|
// Обновляем настройку автопрокрутки для всех открытых логов
|
||||||
Object.keys(state.open).forEach(id => {
|
Object.keys(state.open).forEach(id => {
|
||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
@ -1690,56 +2085,48 @@ els.autoscroll.onchange = ()=> {
|
|||||||
logContent.scrollTop = logContent.scrollHeight;
|
logContent.scrollTop = logContent.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
els.pause.onchange = ()=> {
|
|
||||||
// При снятии паузы показываем накопленные логи с учетом фильтров
|
|
||||||
if (!els.pause.checked) {
|
|
||||||
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) {
|
if (els.filter) {
|
||||||
els.logContent.innerHTML = obj.logEl.innerHTML;
|
els.filter.oninput = ()=> {
|
||||||
const logContent = document.querySelector('.log-content');
|
|
||||||
if (logContent && els.autoscroll.checked) {
|
|
||||||
logContent.scrollTop = logContent.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
els.filter.oninput = ()=> {
|
|
||||||
state.filter = els.filter.value.trim();
|
state.filter = els.filter.value.trim();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
};
|
};
|
||||||
els.lvlDebug.onchange = ()=> {
|
}
|
||||||
|
// Обработчики для LogLevels (если элементы существуют)
|
||||||
|
if (els.lvlDebug) {
|
||||||
|
els.lvlDebug.onchange = ()=> {
|
||||||
state.levels.debug = els.lvlDebug.checked;
|
state.levels.debug = els.lvlDebug.checked;
|
||||||
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
};
|
};
|
||||||
els.lvlInfo.onchange = ()=> {
|
}
|
||||||
|
if (els.lvlInfo) {
|
||||||
|
els.lvlInfo.onchange = ()=> {
|
||||||
state.levels.info = els.lvlInfo.checked;
|
state.levels.info = els.lvlInfo.checked;
|
||||||
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
};
|
};
|
||||||
els.lvlWarn.onchange = ()=> {
|
}
|
||||||
|
if (els.lvlWarn) {
|
||||||
|
els.lvlWarn.onchange = ()=> {
|
||||||
state.levels.warn = els.lvlWarn.checked;
|
state.levels.warn = els.lvlWarn.checked;
|
||||||
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
};
|
};
|
||||||
els.lvlErr.onchange = ()=> {
|
}
|
||||||
|
if (els.lvlErr) {
|
||||||
|
els.lvlErr.onchange = ()=> {
|
||||||
state.levels.err = els.lvlErr.checked;
|
state.levels.err = els.lvlErr.checked;
|
||||||
|
updateCounterVisibility();
|
||||||
refreshAllLogs();
|
refreshAllLogs();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Hotkeys: [ ] — tabs, M — multi
|
// Hotkeys: [ ] — tabs, M — multi, R — refresh
|
||||||
window.addEventListener('keydown', (e)=>{
|
window.addEventListener('keydown', (e)=>{
|
||||||
if (e.key==='[' || (e.ctrlKey && e.key==='ArrowLeft')){
|
if (e.key==='[' || (e.ctrlKey && e.key==='ArrowLeft')){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -1756,6 +2143,10 @@ window.addEventListener('keydown', (e)=>{
|
|||||||
const ans = prompt('IDs через запятую:\n'+list);
|
const ans = prompt('IDs через запятую:\n'+list);
|
||||||
if (ans) openMulti(ans.split(',').map(x=>x.trim()).filter(Boolean));
|
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
|
themeSwitch: !!els.themeSwitch
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Проверяем header project select
|
||||||
|
const headerSelect = document.getElementById('projectSelectHeader');
|
||||||
|
console.log('Header project select found during init:', !!headerSelect);
|
||||||
|
|
||||||
await fetchProjects();
|
await fetchProjects();
|
||||||
await fetchServices();
|
await fetchServices();
|
||||||
|
|
||||||
|
// Инициализируем видимость счетчиков
|
||||||
|
updateCounterVisibility();
|
||||||
|
|
||||||
|
// Добавляем обработчики для счетчиков
|
||||||
|
addCounterClickHandlers();
|
||||||
|
|
||||||
|
// Добавляем обработчик для выпадающего списка проектов в заголовке
|
||||||
|
addHeaderProjectSelectHandler();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user