Přidáno:
nová stránka /portal/incidents s title CSBot Services Portal - Incidenty menu Provoz -> Incidenty aktivní incidenty a historie posledních 100 incidentů barevné stavy: open červeně, resolved zeleně karta Aktivní incidenty a seznam Poslední incidenty na přehledu realtime napojení přes existující operations-live.js a snapshot sekce Incidenty služby v detailu služby, posledních 20 incidentů DB helpery pro service_incidents migrace/indexy pro lokální prostředí, kde tabulka ještě neexistuje
This commit is contained in:
@@ -29,8 +29,10 @@ from ..db.apps import (
|
||||
)
|
||||
from ..db.audit import log_audit_event
|
||||
from ..db.health import get_latest_service_health, get_service_health, get_service_health_history
|
||||
from ..db.incidents import get_service_incidents
|
||||
from ..db.jobs import create_job, get_jobs, has_active_deploy_job
|
||||
from ..routes.deployments import render_status_pill
|
||||
from ..routes.incidents import render_incident_history_rows
|
||||
from ..shell import run_command
|
||||
from ..templates.layout import page, render_result
|
||||
|
||||
@@ -415,6 +417,7 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
templates = get_app_templates()
|
||||
template_options = render_template_options(templates, app.get("template", "") or "")
|
||||
variables = get_app_variables(app.get("id", ""))
|
||||
incidents = get_service_incidents(app.get("id", ""), limit=20)
|
||||
current_health = get_service_health(app.get("id", ""))
|
||||
health_history = get_service_health_history(app.get("id", ""), limit=50)
|
||||
|
||||
@@ -653,6 +656,20 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)):
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Incidenty služby</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th>Začátek</th>
|
||||
<th>Konec</th>
|
||||
<th>Trvání</th>
|
||||
<th>Stav</th>
|
||||
</tr>
|
||||
{render_incident_history_rows(incidents, include_service=False)}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Historie nasazení</h2>
|
||||
<table>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import html
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.auth import require_user
|
||||
from app.db.audit import log_audit_event
|
||||
from app.db.incidents import get_open_incidents, get_recent_incidents
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def format_duration(seconds) -> str:
|
||||
if seconds is None or seconds == "":
|
||||
return ""
|
||||
try:
|
||||
total = max(0, int(seconds))
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
|
||||
hours, remainder = divmod(total, 3600)
|
||||
minutes, secs = divmod(remainder, 60)
|
||||
if hours:
|
||||
return f"{hours} h {minutes} min"
|
||||
if minutes:
|
||||
return f"{minutes} min {secs} s"
|
||||
return f"{secs} s"
|
||||
|
||||
|
||||
def render_incident_status(status: str | None) -> str:
|
||||
value = status or ""
|
||||
normalized = value.lower()
|
||||
labels = {
|
||||
"open": "otev\u0159en\u00fd",
|
||||
"resolved": "vy\u0159e\u0161en\u00fd",
|
||||
}
|
||||
class_name = "pill pill-muted"
|
||||
if normalized == "open":
|
||||
class_name = "pill pill-danger"
|
||||
elif normalized == "resolved":
|
||||
class_name = "pill pill-success"
|
||||
return f'<span class="{class_name}">{html.escape(labels.get(normalized, value))}</span>'
|
||||
|
||||
|
||||
def render_open_incident_rows(incidents: list[dict]) -> str:
|
||||
rows = ""
|
||||
for incident in incidents:
|
||||
service_id = incident.get("service_id", "") or ""
|
||||
service_name = html.escape(incident.get("service_name") or service_id)
|
||||
title = html.escape(incident.get("title", "") or "")
|
||||
started_at = html.escape(incident.get("started_at", "") or "")
|
||||
duration = format_duration(incident.get("duration_seconds"))
|
||||
if not duration:
|
||||
duration = "prob\u00edh\u00e1"
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td><a href="/portal/apps/{quote(service_id, safe='')}">{service_name}</a></td>
|
||||
<td>{title}</td>
|
||||
<td>{started_at}</td>
|
||||
<td>{html.escape(duration)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="4">\u017d\u00e1dn\u00e9 aktivn\u00ed incidenty.</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
def render_incident_history_rows(incidents: list[dict], include_service: bool = True) -> str:
|
||||
rows = ""
|
||||
for incident in incidents:
|
||||
service_id = incident.get("service_id", "") or ""
|
||||
service_name = html.escape(incident.get("service_name") or service_id)
|
||||
title = html.escape(incident.get("title", "") or "")
|
||||
started_at = html.escape(incident.get("started_at", "") or "")
|
||||
ended_at = html.escape(incident.get("ended_at", "") or "")
|
||||
duration = html.escape(format_duration(incident.get("duration_seconds")))
|
||||
status = render_incident_status(incident.get("status"))
|
||||
service_cell = f'<td><a href="/portal/apps/{quote(service_id, safe="")}">{service_name}</a></td>' if include_service else ""
|
||||
rows += f"""
|
||||
<tr>
|
||||
{service_cell}
|
||||
<td>{title}</td>
|
||||
<td>{started_at}</td>
|
||||
<td>{ended_at}</td>
|
||||
<td>{duration}</td>
|
||||
<td>{status}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
colspan = 6 if include_service else 5
|
||||
if not rows:
|
||||
rows = f'<tr><td colspan="{colspan}">Zat\u00edm nejsou evidovan\u00e9 \u017e\u00e1dn\u00e9 incidenty.</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
@router.get("/incidents", response_class=HTMLResponse)
|
||||
def incidents_page(request: Request, user=Depends(require_user)):
|
||||
open_incidents = get_open_incidents()
|
||||
recent_incidents = get_recent_incidents(limit=100)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="incidents.view",
|
||||
target_type="incidents",
|
||||
metadata={"open": len(open_incidents), "history": len(recent_incidents)},
|
||||
)
|
||||
|
||||
return page(
|
||||
"Incidenty",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2>Incidenty</h2>
|
||||
<p class="muted">P\u0159ehled incident\u016f vytv\u00e1\u0159en\u00fdch a uzav\u00edran\u00fdch monitorem.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Aktivn\u00ed incidenty</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Slu\u017eba</th>
|
||||
<th>N\u00e1zev</th>
|
||||
<th>Za\u010d\u00e1tek</th>
|
||||
<th>Trv\u00e1n\u00ed</th>
|
||||
</tr>
|
||||
<tbody data-live-table="incidents.active">{render_open_incident_rows(open_incidents)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Historie incident\u016f</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Slu\u017eba</th>
|
||||
<th>N\u00e1zev</th>
|
||||
<th>Za\u010d\u00e1tek</th>
|
||||
<th>Konec</th>
|
||||
<th>Trv\u00e1n\u00ed</th>
|
||||
<th>Stav</th>
|
||||
</tr>
|
||||
<tbody data-live-table="incidents.recent">{render_incident_history_rows(recent_incidents)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="/portal/static/operations-live.js"></script>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
@@ -11,6 +11,7 @@ from app.db.operations import (
|
||||
get_operations_snapshot,
|
||||
)
|
||||
from app.routes.deployments import render_status_pill
|
||||
from app.routes.incidents import render_incident_history_rows, render_open_incident_rows
|
||||
from app.routes.jobs import render_job_status
|
||||
from app.templates.layout import page
|
||||
|
||||
@@ -171,6 +172,9 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
"running": snapshot["deployments"]["running"],
|
||||
"failed_24h": snapshot["deployments"]["failed_24h"],
|
||||
},
|
||||
"incidents": {
|
||||
"open": snapshot["incidents"]["open"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -188,6 +192,7 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
<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-danger"><span>Aktivní incidenty</span><strong data-live-count="incidents.open">{snapshot["incidents"]["open"]}</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>
|
||||
@@ -209,17 +214,15 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední incidenty</h2>
|
||||
<h2>Aktivní incidenty</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Typ</th>
|
||||
<th>Cíl</th>
|
||||
<th>Zdroj</th>
|
||||
<th>Vytvořeno</th>
|
||||
<th>Služba</th>
|
||||
<th>Název</th>
|
||||
<th>Začátek</th>
|
||||
<th>Trvání</th>
|
||||
</tr>
|
||||
<tbody data-live-table="jobs.failed">{render_recent_jobs(snapshot["jobs"]["recent_failed"])}</tbody>
|
||||
<tbody data-live-table="incidents.active">{render_open_incident_rows(snapshot["incidents"]["active"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -239,6 +242,21 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední incidenty</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Služba</th>
|
||||
<th>Název</th>
|
||||
<th>Začátek</th>
|
||||
<th>Konec</th>
|
||||
<th>Trvání</th>
|
||||
<th>Stav</th>
|
||||
</tr>
|
||||
<tbody data-live-table="incidents.recent">{render_incident_history_rows(snapshot["incidents"]["recent"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Poslední auditní události</h2>
|
||||
<table>
|
||||
|
||||
Reference in New Issue
Block a user