ui: синхронизация каталога Grafana в браузере, дерево папок/правил; инциденты — ссылка, деталка, сырой JSON; ci: docker compose --progress plain
All checks were successful
CI / test (push) Successful in 37s
All checks were successful
CI / test (push) Successful in 37s
Made-with: Cursor
This commit is contained in:
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
@ -261,6 +262,11 @@ async def patch_incident_api(
|
||||
return _incident_row_dict(row)
|
||||
|
||||
|
||||
def _title_cell(raw: object) -> str:
|
||||
t = (str(raw).strip() if raw is not None else "") or "—"
|
||||
return html.escape(t)
|
||||
|
||||
|
||||
@ui_router.get("/", response_class=HTMLResponse)
|
||||
async def incidents_ui_home(request: Request):
|
||||
pool = get_pool(request)
|
||||
@ -280,17 +286,22 @@ async def incidents_ui_home(request: Request):
|
||||
"""
|
||||
)
|
||||
for r in rows:
|
||||
iid = r["id"]
|
||||
iid_s = str(iid)
|
||||
org = html.escape(str(r["grafana_org_slug"] or "—"))
|
||||
svc = html.escape(str(r["service_name"] or "—"))
|
||||
ca = r["created_at"].isoformat() if r["created_at"] else "—"
|
||||
rows_html += (
|
||||
"<tr>"
|
||||
f"<td>{html.escape(str(r['id']))[:8]}…</td>"
|
||||
f"<td>{html.escape(r['title'])}</td>"
|
||||
f"<td><a href=\"/ui/modules/incidents/{html.escape(iid_s, quote=True)}\">"
|
||||
f"{html.escape(iid_s[:8])}…</a></td>"
|
||||
f"<td>{_title_cell(r['title'])}</td>"
|
||||
f"<td>{html.escape(r['status'])}</td>"
|
||||
f"<td>{html.escape(r['severity'])}</td>"
|
||||
f"<td>{html.escape(r['source'])}</td>"
|
||||
f"<td>{org}</td>"
|
||||
f"<td>{svc}</td>"
|
||||
f"<td>{html.escape(ca)}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
except Exception as e:
|
||||
@ -298,10 +309,10 @@ async def incidents_ui_home(request: Request):
|
||||
inner = f"""<h1>Инциденты</h1>
|
||||
{err}
|
||||
<table class="irm-table">
|
||||
<thead><tr><th>ID</th><th>Заголовок</th><th>Статус</th><th>Важность</th><th>Источник</th><th>Grafana slug</th><th>Сервис</th></tr></thead>
|
||||
<tbody>{rows_html or '<tr><td colspan="7">Пока нет записей</td></tr>'}</tbody>
|
||||
<thead><tr><th>ID</th><th>Заголовок</th><th>Статус</th><th>Важность</th><th>Источник</th><th>Grafana slug</th><th>Сервис</th><th>Создан</th></tr></thead>
|
||||
<tbody>{rows_html or '<tr><td colspan="8">Пока нет записей</td></tr>'}</tbody>
|
||||
</table>
|
||||
<p><small>Создание из Grafana: webhook → запись в <code>ingress_events</code> → событие → строка здесь.</small></p>"""
|
||||
<p><small>Создание из Grafana: webhook → <code>ingress_events</code> → событие → строка здесь. Пустой заголовок бывает при тестовом JSON без полей алерта.</small></p>"""
|
||||
return HTMLResponse(
|
||||
wrap_module_html_page(
|
||||
document_title="Инциденты — onGuard24",
|
||||
@ -309,3 +320,90 @@ async def incidents_ui_home(request: Request):
|
||||
main_inner_html=inner,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ui_router.get("/{incident_id:uuid}", response_class=HTMLResponse)
|
||||
async def incident_detail_ui(request: Request, incident_id: UUID):
|
||||
pool = get_pool(request)
|
||||
if pool is None:
|
||||
body = "<p>База данных не настроена.</p>"
|
||||
return HTMLResponse(
|
||||
wrap_module_html_page(
|
||||
document_title="Инцидент — onGuard24",
|
||||
current_slug="incidents",
|
||||
main_inner_html=f"<h1>Инцидент</h1>{body}",
|
||||
)
|
||||
)
|
||||
try:
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id, title, status, severity, source, ingress_event_id, created_at, updated_at,
|
||||
grafana_org_slug, service_name
|
||||
FROM incidents WHERE id = $1::uuid
|
||||
""",
|
||||
incident_id,
|
||||
)
|
||||
raw_row = None
|
||||
if row and row.get("ingress_event_id"):
|
||||
raw_row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id, source, received_at, body, org_slug, service_name
|
||||
FROM ingress_events WHERE id = $1::uuid
|
||||
""",
|
||||
row["ingress_event_id"],
|
||||
)
|
||||
except Exception as e:
|
||||
return HTMLResponse(
|
||||
wrap_module_html_page(
|
||||
document_title="Инцидент — onGuard24",
|
||||
current_slug="incidents",
|
||||
main_inner_html=f"<h1>Инцидент</h1><p class='module-err'>{html.escape(str(e))}</p>",
|
||||
)
|
||||
)
|
||||
if not row:
|
||||
body = "<p>Запись не найдена.</p>"
|
||||
else:
|
||||
title = _title_cell(row["title"])
|
||||
ing = row["ingress_event_id"]
|
||||
ing_l = html.escape(str(ing)) if ing else "—"
|
||||
meta = f"""<dl style="display:grid;grid-template-columns:10rem 1fr;gap:0.35rem 1rem;font-size:0.9rem">
|
||||
<dt>ID</dt><dd><code>{html.escape(str(row['id']))}</code></dd>
|
||||
<dt>Заголовок</dt><dd>{title}</dd>
|
||||
<dt>Статус</dt><dd>{html.escape(row['status'])}</dd>
|
||||
<dt>Важность</dt><dd>{html.escape(row['severity'])}</dd>
|
||||
<dt>Источник</dt><dd>{html.escape(row['source'])}</dd>
|
||||
<dt>Grafana slug</dt><dd>{html.escape(str(row['grafana_org_slug'] or '—'))}</dd>
|
||||
<dt>Сервис</dt><dd>{html.escape(str(row['service_name'] or '—'))}</dd>
|
||||
<dt>Создан</dt><dd>{html.escape(row['created_at'].isoformat() if row['created_at'] else '—')}</dd>
|
||||
<dt>Обновлён</dt><dd>{html.escape(row['updated_at'].isoformat() if row.get('updated_at') else '—')}</dd>
|
||||
<dt>ingress_event_id</dt><dd><code>{ing_l}</code></dd>
|
||||
</dl>"""
|
||||
raw_block = ""
|
||||
if raw_row:
|
||||
try:
|
||||
body_obj = raw_row["body"]
|
||||
if hasattr(body_obj, "keys"):
|
||||
pretty = json.dumps(dict(body_obj), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
pretty = str(body_obj)
|
||||
if len(pretty) > 12000:
|
||||
pretty = pretty[:12000] + "\n…"
|
||||
raw_block = (
|
||||
"<h2 style='font-size:1.05rem;margin-top:1.25rem'>Сырой JSON вебхука</h2>"
|
||||
f"<p class='gc-muted'>ingress_events · {html.escape(str(raw_row['received_at']))}</p>"
|
||||
f"<pre style='overflow:auto;max-height:28rem;font-size:0.78rem;background:#18181b;color:#e4e4e7;"
|
||||
f"padding:0.75rem;border-radius:8px'>{html.escape(pretty)}</pre>"
|
||||
)
|
||||
except Exception as ex:
|
||||
raw_block = f"<p class='module-err'>Не удалось показать JSON: {html.escape(str(ex))}</p>"
|
||||
body = (
|
||||
f"<p><a href=\"/ui/modules/incidents/\">← К списку</a></p><h1>Инцидент</h1>{meta}{raw_block}"
|
||||
)
|
||||
return HTMLResponse(
|
||||
wrap_module_html_page(
|
||||
document_title="Инцидент — onGuard24",
|
||||
current_slug="incidents",
|
||||
main_inner_html=body,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user