Přidal jsem Health Monitoring do portálu:
Přehled má nové karty: Zdravé služby, Nezdravé služby, Nedostupné služby. Přehled zobrazuje sekci Služby vyžadující pozornost pro unhealthy a unreachable. Snapshot /portal/ws/operations teď obsahuje health data a stránka je aktualizuje přes existující realtime vrstvu. Seznam služeb zobrazuje aktuální health status a poslední kontrolu, včetně řazení Podle zdraví. Detail služby má sekce Zdraví služby a Historie kontrol s posledními 50 záznamy. Přidal jsem audit event service.health.view. Browser title teď používá formát CSBot Services Portal - ..., takže detail služby odpovídá požadavku. Nepoužil jsem ORM, vše je přes SQLite dotazy.
This commit is contained in:
+97
-2
@@ -17,6 +17,7 @@ from ..config import (
|
||||
)
|
||||
from ..db.apps import get_app, get_app_deployments, get_apps, update_app_resources
|
||||
from ..db.audit import log_audit_event
|
||||
from ..db.health import get_latest_service_health, get_service_health, get_service_health_history
|
||||
from ..db.jobs import create_job, get_jobs, has_active_deploy_job
|
||||
from ..routes.deployments import render_status_pill
|
||||
from ..shell import run_command
|
||||
@@ -26,6 +27,24 @@ router = APIRouter()
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
|
||||
def render_health_status(status: str | None) -> str:
|
||||
value = status or ""
|
||||
normalized = value.lower()
|
||||
labels = {
|
||||
"healthy": "zdravá",
|
||||
"unhealthy": "nezdravá",
|
||||
"unreachable": "nedostupná",
|
||||
}
|
||||
class_name = "pill pill-muted"
|
||||
if normalized == "healthy":
|
||||
class_name = "pill pill-success"
|
||||
elif normalized == "unhealthy":
|
||||
class_name = "pill pill-warning"
|
||||
elif normalized == "unreachable":
|
||||
class_name = "pill pill-danger"
|
||||
return f'<span class="{class_name}">{html.escape(labels.get(normalized, value or "neznámá"))}</span>'
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def portal_home(user=Depends(require_user)):
|
||||
return RedirectResponse(url="/portal/operations", status_code=303)
|
||||
@@ -40,6 +59,7 @@ def apps_page(
|
||||
user=Depends(require_user),
|
||||
):
|
||||
apps = get_apps()
|
||||
latest_health = get_latest_service_health()
|
||||
query = q.strip()
|
||||
selected_status = status.strip()
|
||||
if query:
|
||||
@@ -50,6 +70,16 @@ def apps_page(
|
||||
]
|
||||
if selected_status:
|
||||
apps = [item for item in apps if (item.get("status", "") or "") == selected_status]
|
||||
sort = request.query_params.get("sort", "").strip()
|
||||
if sort == "health":
|
||||
order = {"unreachable": 0, "unhealthy": 1, "healthy": 2}
|
||||
apps = sorted(
|
||||
apps,
|
||||
key=lambda item: (
|
||||
order.get((latest_health.get(item.get("id", "")) or {}).get("status"), 3),
|
||||
item.get("id", ""),
|
||||
),
|
||||
)
|
||||
status_values = sorted({item.get("status", "") for item in get_apps() if item.get("status")})
|
||||
total_apps = len(apps)
|
||||
total_pages = max(1, math.ceil(total_apps / DEFAULT_PAGE_SIZE))
|
||||
@@ -68,6 +98,9 @@ def apps_page(
|
||||
app_id = html.escape(item.get("id", ""))
|
||||
app_url_id = quote(item.get("id", ""), safe="")
|
||||
status = html.escape(item.get("status", ""))
|
||||
health = latest_health.get(item.get("id", "")) or {}
|
||||
health_status = render_health_status(health.get("status"))
|
||||
health_checked_at = html.escape(health.get("checked_at", "") or "")
|
||||
docs = html.escape(item.get("docs", f"/apps/{app_id}/docs"))
|
||||
memory = item.get("memory", "")
|
||||
cpus = item.get("cpus", "")
|
||||
@@ -104,6 +137,7 @@ def apps_page(
|
||||
<span class="muted">/apps/{app_id}</span>
|
||||
</td>
|
||||
<td><span class="pill">{status}</span></td>
|
||||
<td>{health_status}<br><span class="muted">{health_checked_at}</span></td>
|
||||
<td><a href="{docs}">Swagger</a></td>
|
||||
<td>
|
||||
<form method="post" action="/portal/update-resources">
|
||||
@@ -150,7 +184,7 @@ def apps_page(
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="6">Zatím nejsou nasazené žádné služby.</td></tr>'
|
||||
rows = '<tr><td colspan="7">Zatím nejsou nasazené žádné služby.</td></tr>'
|
||||
status_options = ['<option value="">Všechny stavy</option>']
|
||||
for value in status_values:
|
||||
selected = " selected" if selected_status == value else ""
|
||||
@@ -165,6 +199,8 @@ def apps_page(
|
||||
params["q"] = query
|
||||
if selected_status:
|
||||
params["status"] = selected_status
|
||||
if sort:
|
||||
params["sort"] = sort
|
||||
return f"/portal/apps?{urlencode(params)}"
|
||||
|
||||
previous_link = (
|
||||
@@ -216,6 +252,10 @@ def apps_page(
|
||||
<form method="get" action="/portal/apps" class="filter-form">
|
||||
<input name="q" value="{html.escape(query)}" placeholder="Název nebo ID služby">
|
||||
<select name="status">{"".join(status_options)}</select>
|
||||
<select name="sort">
|
||||
<option value="">Výchozí řazení</option>
|
||||
<option value="health"{" selected" if sort == "health" else ""}>Podle zdraví</option>
|
||||
</select>
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit">Filtrovat</button>
|
||||
<a class="btn btn-secondary" href="/portal/apps">Reset</a>
|
||||
@@ -225,6 +265,7 @@ def apps_page(
|
||||
<tr>
|
||||
<th>Služba</th>
|
||||
<th>Status</th>
|
||||
<th>Zdraví</th>
|
||||
<th>Dokumentace</th>
|
||||
<th>Prostředky</th>
|
||||
<th>Git</th>
|
||||
@@ -245,6 +286,13 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
log_audit_event(
|
||||
user,
|
||||
action="service.health.view",
|
||||
target_type="service",
|
||||
target_id=app_id,
|
||||
)
|
||||
|
||||
escaped_app_id = html.escape(app.get("id", ""))
|
||||
app_url_id = quote(app.get("id", ""), safe="")
|
||||
name = html.escape(app.get("name", "") or "")
|
||||
@@ -254,6 +302,8 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
memory = html.escape(app.get("memory", "") or "")
|
||||
cpus = html.escape(app.get("cpus", "") or "")
|
||||
updated_at = html.escape(app.get("updated_at", "") or "")
|
||||
current_health = get_service_health(app.get("id", ""))
|
||||
health_history = get_service_health_history(app.get("id", ""), limit=50)
|
||||
|
||||
rows = ""
|
||||
for deployment in get_app_deployments(app.get("id", ""), limit=10):
|
||||
@@ -294,8 +344,29 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
if not job_rows:
|
||||
job_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné úlohy této služby.</td></tr>'
|
||||
|
||||
health_status = render_health_status(current_health.get("status") if current_health else None)
|
||||
health_http_status = html.escape(str(current_health.get("http_status") or "")) if current_health else ""
|
||||
health_response_time = html.escape(str(current_health.get("response_time_ms") or "")) if current_health else ""
|
||||
health_checked_at = html.escape(current_health.get("checked_at", "") or "") if current_health else ""
|
||||
health_rows = ""
|
||||
for item in health_history:
|
||||
error_text = item.get("error_text", "") or ""
|
||||
error_preview = error_text if len(error_text) <= 140 else f"{error_text[:137]}..."
|
||||
health_rows += f"""
|
||||
<tr>
|
||||
<td>{html.escape(item.get("checked_at", "") or "")}</td>
|
||||
<td>{render_health_status(item.get("status"))}</td>
|
||||
<td>{html.escape(str(item.get("http_status") or ""))}</td>
|
||||
<td>{html.escape(str(item.get("response_time_ms") or ""))}</td>
|
||||
<td>{html.escape(error_preview)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not health_rows:
|
||||
health_rows = '<tr><td colspan="5">Zatím nejsou evidované žádné kontroly zdraví.</td></tr>'
|
||||
|
||||
return page(
|
||||
f"Služba {escaped_app_id}",
|
||||
name or escaped_app_id,
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>{escaped_app_id}</h2>
|
||||
@@ -325,6 +396,30 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Zdraví služby</h2>
|
||||
<table>
|
||||
<tr><th>Aktuální status</th><td>{health_status}</td></tr>
|
||||
<tr><th>HTTP status</th><td>{health_http_status}</td></tr>
|
||||
<tr><th>Odezva</th><td>{health_response_time}</td></tr>
|
||||
<tr><th>Poslední kontrola</th><td>{health_checked_at}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Historie kontrol</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Čas</th>
|
||||
<th>Status</th>
|
||||
<th>HTTP status</th>
|
||||
<th>Odezva</th>
|
||||
<th>Chyba</th>
|
||||
</tr>
|
||||
{health_rows}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Historie nasazení</h2>
|
||||
<table>
|
||||
|
||||
@@ -110,6 +110,46 @@ def render_recent_audit_events(events: list[dict]) -> str:
|
||||
return rows
|
||||
|
||||
|
||||
def render_health_status(status: str | None) -> str:
|
||||
value = status or ""
|
||||
normalized = value.lower()
|
||||
labels = {
|
||||
"healthy": "zdravá",
|
||||
"unhealthy": "nezdravá",
|
||||
"unreachable": "nedostupná",
|
||||
}
|
||||
class_name = "pill pill-muted"
|
||||
if normalized == "healthy":
|
||||
class_name = "pill pill-success"
|
||||
elif normalized == "unhealthy":
|
||||
class_name = "pill pill-warning"
|
||||
elif normalized == "unreachable":
|
||||
class_name = "pill pill-danger"
|
||||
return f'<span class="{class_name}">{html.escape(labels.get(normalized, value))}</span>'
|
||||
|
||||
|
||||
def render_health_problems(items: list[dict]) -> str:
|
||||
rows = ""
|
||||
for item in items:
|
||||
service_id = html.escape(item.get("service_id", "") or "")
|
||||
service_name = html.escape(item.get("service_name") or item.get("service_id", "") or "")
|
||||
error_text = item.get("error_text", "") or ""
|
||||
error_preview = error_text if len(error_text) <= 120 else f"{error_text[:117]}..."
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td><a href="/portal/apps/{quote(service_id, safe='')}">{service_name}</a></td>
|
||||
<td>{render_health_status(item.get("status"))}</td>
|
||||
<td>{html.escape(item.get("checked_at", "") or "")}</td>
|
||||
<td>{html.escape(str(item.get("response_time_ms") or ""))}</td>
|
||||
<td>{html.escape(error_preview)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="5">Žádné služby nevyžadují pozornost.</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
@router.get("/operations", response_class=HTMLResponse)
|
||||
def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
snapshot = get_operations_snapshot()
|
||||
@@ -145,12 +185,29 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-success"><span>Běžící služby</span><strong data-live-count="apps.active">{snapshot["apps"]["active"]}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Problémové služby</span><strong data-live-count="apps.problematic">{snapshot["apps"]["problematic"]}</strong></div>
|
||||
<div class="stat-card stat-success"><span>Zdravé služby</span><strong data-live-count="health.healthy">{snapshot["health"]["healthy"]}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Nezdravé služby</span><strong data-live-count="health.unhealthy">{snapshot["health"]["unhealthy"]}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Nedostupné služby</span><strong data-live-count="health.unreachable">{snapshot["health"]["unreachable"]}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Aktivní úlohy</span><strong data-live-count="jobs.running">{snapshot["jobs"]["running"]}</strong></div>
|
||||
<div class="stat-card stat-total"><span>Nasazení dnes</span><strong data-live-count="deployments.today">{snapshot["deployments"]["today"]}</strong></div>
|
||||
<div class="stat-card stat-success"><span>Online workery</span><strong data-live-count="workers.online">{snapshot["workers"]["online"]}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Selhané úlohy (24 h)</span><strong data-live-count="jobs.failed_24h">{snapshot["jobs"]["failed_24h"]}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Služby vyžadující pozornost</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Služba</th>
|
||||
<th>Status</th>
|
||||
<th>Poslední kontrola</th>
|
||||
<th>Odezva</th>
|
||||
<th>Chyba</th>
|
||||
</tr>
|
||||
<tbody data-live-table="health.problems">{render_health_problems(snapshot["health"]["problems"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední incidenty</h2>
|
||||
<table>
|
||||
|
||||
Reference in New Issue
Block a user