(() => {
if (window.CSBotOperationsLive) {
return;
}
const state = {
socket: null,
reconnectTimer: null,
reconnectDelay: 1500,
};
const escapeHtml = (value) => String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const statusBadge = (status) => {
const value = String(status || "");
const normalized = value.toLowerCase();
const labels = {
queued: "čeká",
running: "běží",
cancelled_requested: "žádost o zrušení",
cancelled: "zrušeno",
success: "úspěšné",
failed: "selhalo",
ok: "úspěšné",
error: "chyba",
};
let className = "pill pill-muted";
if (["success", "ok", "succeeded", "done", "deployed", "completed"].includes(normalized)) {
className = "pill pill-success";
} else if (["queued", "running", "pending", "in_progress", "starting", "cancelled_requested"].includes(normalized)) {
className = "pill pill-warning";
} else if (["failed", "failure", "error", "cancelled", "canceled"].includes(normalized)) {
className = "pill pill-danger";
}
return `${escapeHtml(labels[normalized] || value)}`;
};
const workerBadge = (online) => online
? 'online'
: 'offline';
const healthBadge = (status) => {
const value = String(status || "");
const normalized = value.toLowerCase();
const labels = {
healthy: "zdravá",
unhealthy: "nezdravá",
unreachable: "nedostupná",
};
let className = "pill pill-muted";
if (normalized === "healthy") className = "pill pill-success";
if (normalized === "unhealthy") className = "pill pill-warning";
if (normalized === "unreachable") className = "pill pill-danger";
return `${escapeHtml(labels[normalized] || value)}`;
};
const setText = (selector, value) => {
document.querySelectorAll(selector).forEach((el) => {
el.textContent = value ?? 0;
});
};
const renderJobs = (jobs) => {
const rows = (jobs || []).map((job) => `
| #${escapeHtml(job.id)} |
${statusBadge(job.status)} |
${escapeHtml(job.type)} |
${escapeHtml(job.target_type)}: ${escapeHtml(job.target_id)} |
${escapeHtml(job.source)} |
${escapeHtml(job.created_at)} |
`).join("");
return rows || '| Zatím nejsou evidované žádné úlohy. |
';
};
const renderWorkers = (workers) => {
const rows = (workers || []).map((worker) => {
const currentJob = worker.current_job_id === null || worker.current_job_id === undefined
? ""
: `#${escapeHtml(worker.current_job_id)}`;
return `
| ${escapeHtml(worker.id)} |
${workerBadge(Boolean(worker.online))} ${escapeHtml(worker.status)} |
${escapeHtml(worker.last_seen_at)} |
${currentJob} |
${escapeHtml(worker.started_at)} |
Detail |
`;
}).join("");
return rows || '| Zatím nejsou evidovaní žádní workeři. |
';
};
const renderDeployments = (deployments) => {
const rows = (deployments || []).map((deployment) => `
| #${escapeHtml(deployment.id)} |
${escapeHtml(deployment.app_id)} |
${escapeHtml(deployment.kind)} |
${statusBadge(deployment.status)} |
${escapeHtml(deployment.trigger_source)} |
${escapeHtml(deployment.started_at)} |
${escapeHtml(deployment.finished_at)} |
`).join("");
return rows || '| Zatím nejsou evidovaná žádná nasazení. |
';
};
const renderAudit = (events) => {
const rows = (events || []).map((event) => `
| ${escapeHtml(event.created_at)} |
${escapeHtml(event.username)} |
${escapeHtml(event.action)} |
${escapeHtml(event.target_type)} |
${escapeHtml(event.target_id)} |
${escapeHtml(event.source)} |
`).join("");
return rows || '| Zatím nejsou evidované žádné auditní události. |
';
};
const renderHealthProblems = (items) => {
const rows = (items || []).map((item) => {
const errorText = String(item.error_text || "");
const errorPreview = errorText.length > 120 ? `${errorText.slice(0, 117)}...` : errorText;
const serviceName = item.service_name || item.service_id || "";
return `
| ${escapeHtml(serviceName)} |
${healthBadge(item.status)} |
${escapeHtml(item.checked_at)} |
${escapeHtml(item.response_time_ms ?? "")} |
${escapeHtml(errorPreview)} |
`;
}).join("");
return rows || '| Žádné služby nevyžadují pozornost. |
';
};
const renderSnapshot = (snapshot) => {
setText("[data-live-count='jobs.queued']", snapshot.jobs?.queued);
setText("[data-live-count='jobs.running']", snapshot.jobs?.running);
setText("[data-live-count='jobs.failed_24h']", snapshot.jobs?.failed_24h);
setText("[data-live-count='jobs.success_24h']", snapshot.jobs?.success_24h);
setText("[data-live-count='apps.active']", snapshot.apps?.active);
setText("[data-live-count='apps.problematic']", snapshot.apps?.problematic);
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='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));
setText("[data-live-count='deployments.running']", snapshot.deployments?.running);
setText("[data-live-count='deployments.failed_24h']", snapshot.deployments?.failed_24h);
setText("[data-live-count='deployments.success_24h']", snapshot.deployments?.success_24h);
setText("[data-live-count='deployments.today']", snapshot.deployments?.today);
document.querySelectorAll("[data-live-table='jobs.recent']").forEach((el) => { el.innerHTML = renderJobs(snapshot.jobs?.recent); });
document.querySelectorAll("[data-live-table='jobs.failed']").forEach((el) => { el.innerHTML = renderJobs(snapshot.jobs?.recent_failed); });
document.querySelectorAll("[data-live-table='workers.recent']").forEach((el) => { el.innerHTML = renderWorkers(snapshot.workers?.recent); });
document.querySelectorAll("[data-live-table='deployments.recent']").forEach((el) => { el.innerHTML = renderDeployments(snapshot.deployments?.recent); });
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); });
(snapshot.workers?.recent || []).forEach((worker) => {
const row = Array.from(document.querySelectorAll("tr[data-worker-id]"))
.find((item) => item.dataset.workerId === String(worker.id || ""));
if (!row) {
return;
}
const currentJob = worker.current_job_id === null || worker.current_job_id === undefined
? ""
: `#${escapeHtml(worker.current_job_id)}`;
const statusCell = row.querySelector("[data-worker-field='status']");
const lastSeenCell = row.querySelector("[data-worker-field='last_seen_at']");
const currentJobCell = row.querySelector("[data-worker-field='current_job']");
if (statusCell) statusCell.innerHTML = `${workerBadge(Boolean(worker.online))}
${escapeHtml(worker.status)}`;
if (lastSeenCell) lastSeenCell.textContent = worker.last_seen_at || "";
if (currentJobCell) currentJobCell.innerHTML = currentJob;
});
};
const connect = () => {
clearTimeout(state.reconnectTimer);
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
state.socket = new WebSocket(`${protocol}//${window.location.host}/portal/ws/operations`);
state.socket.addEventListener("message", (event) => {
const snapshot = JSON.parse(event.data);
if (snapshot.type !== "operations.snapshot") {
return;
}
window.dispatchEvent(new CustomEvent("operations:snapshot", { detail: snapshot }));
renderSnapshot(snapshot);
});
state.socket.addEventListener("close", () => {
state.reconnectTimer = setTimeout(connect, state.reconnectDelay);
});
state.socket.addEventListener("error", () => {
state.socket.close();
});
};
window.CSBotOperationsLive = { connect };
connect();
})();