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:
JiriUhlir
2026-06-01 13:38:06 +02:00
parent 994171be1a
commit 6aa5e69ee4
9 changed files with 374 additions and 9 deletions
+62
View File
@@ -59,6 +59,32 @@
return `<span class="${className}">${escapeHtml(labels[normalized] || value)}</span>`;
};
const incidentBadge = (status) => {
const value = String(status || "");
const normalized = value.toLowerCase();
const labels = {
open: "otevřený",
resolved: "vyřešený",
};
let className = "pill pill-muted";
if (normalized === "open") className = "pill pill-danger";
if (normalized === "resolved") className = "pill pill-success";
return `<span class="${className}">${escapeHtml(labels[normalized] || value)}</span>`;
};
const formatDuration = (seconds) => {
if (seconds === null || seconds === undefined || seconds === "") {
return "";
}
const total = Math.max(0, Number.parseInt(seconds, 10) || 0);
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const secs = total % 60;
if (hours) return `${hours} h ${minutes} min`;
if (minutes) return `${minutes} min ${secs} s`;
return `${secs} s`;
};
const setText = (selector, value) => {
document.querySelectorAll(selector).forEach((el) => {
el.textContent = value ?? 0;
@@ -145,6 +171,39 @@
return rows || '<tr><td colspan="5">Žádné služby nevyžadují pozornost.</td></tr>';
};
const renderOpenIncidents = (items) => {
const rows = (items || []).map((item) => {
const serviceName = item.service_name || item.service_id || "";
const duration = formatDuration(item.duration_seconds) || "probíhá";
return `
<tr>
<td><a href="/portal/apps/${encodeURIComponent(item.service_id || "")}">${escapeHtml(serviceName)}</a></td>
<td>${escapeHtml(item.title)}</td>
<td>${escapeHtml(item.started_at)}</td>
<td>${escapeHtml(duration)}</td>
</tr>
`;
}).join("");
return rows || '<tr><td colspan="4">Žádné aktivní incidenty.</td></tr>';
};
const renderIncidents = (items) => {
const rows = (items || []).map((item) => {
const serviceName = item.service_name || item.service_id || "";
return `
<tr>
<td><a href="/portal/apps/${encodeURIComponent(item.service_id || "")}">${escapeHtml(serviceName)}</a></td>
<td>${escapeHtml(item.title)}</td>
<td>${escapeHtml(item.started_at)}</td>
<td>${escapeHtml(item.ended_at)}</td>
<td>${escapeHtml(formatDuration(item.duration_seconds))}</td>
<td>${incidentBadge(item.status)}</td>
</tr>
`;
}).join("");
return rows || '<tr><td colspan="6">Zatím nejsou evidované žádné incidenty.</td></tr>';
};
const renderSnapshot = (snapshot) => {
setText("[data-live-count='jobs.queued']", snapshot.jobs?.queued);
setText("[data-live-count='jobs.running']", snapshot.jobs?.running);
@@ -155,6 +214,7 @@
setText("[data-live-count='health.healthy']", snapshot.health?.healthy);
setText("[data-live-count='health.unhealthy']", snapshot.health?.unhealthy);
setText("[data-live-count='health.unreachable']", snapshot.health?.unreachable);
setText("[data-live-count='incidents.open']", snapshot.incidents?.open);
setText("[data-live-count='workers.online']", snapshot.workers?.online);
setText("[data-live-count='workers.offline']", snapshot.workers?.offline);
setText("[data-live-count='workers.total']", (snapshot.workers?.online || 0) + (snapshot.workers?.offline || 0));
@@ -170,6 +230,8 @@
document.querySelectorAll("[data-live-table='deployments.failed']").forEach((el) => { el.innerHTML = renderDeployments(snapshot.deployments?.recent_failed); });
document.querySelectorAll("[data-live-table='audit.recent']").forEach((el) => { el.innerHTML = renderAudit(snapshot.audit?.recent); });
document.querySelectorAll("[data-live-table='health.problems']").forEach((el) => { el.innerHTML = renderHealthProblems(snapshot.health?.problems); });
document.querySelectorAll("[data-live-table='incidents.active']").forEach((el) => { el.innerHTML = renderOpenIncidents(snapshot.incidents?.active); });
document.querySelectorAll("[data-live-table='incidents.recent']").forEach((el) => { el.innerHTML = renderIncidents(snapshot.incidents?.recent); });
(snapshot.workers?.recent || []).forEach((worker) => {
const row = Array.from(document.querySelectorAll("tr[data-worker-id]"))