feat(portal): add agent coverage sla

This commit is contained in:
igor04091968
2026-06-04 12:41:03 +03:00
parent bb00651bce
commit a30ab9cc95
6 changed files with 596 additions and 2 deletions
@@ -24,9 +24,9 @@ async function postJson(path, payload) {
function statusClass(status) {
const s = String(status || "UNKNOWN").toLowerCase();
if (s === "ok" || s === "true") return "status-ok";
if (s === "warn" || s === "warning" || s === "fallback") return "status-warn";
if (s === "warn" || s === "warning" || s === "fallback" || s === "stale") return "status-warn";
if (s === "degraded") return "status-degraded";
if (s === "fail" || s === "false" || s === "error") return "status-fail";
if (s === "fail" || s === "false" || s === "error" || s === "critical" || s === "missing") return "status-fail";
return "status-unknown";
}
@@ -828,6 +828,7 @@ function renderOperator(data, report) {
${renderAgentQuality(report?.agent_quality, report?.agent_quality_explain)}
${renderAgentQualityHistory(report?.agent_quality_history, report?.agent_quality_history_summary)}
${renderAgentQualityNodes(report?.agent_quality_nodes, report?.agent_quality_nodes_summary)}
${renderAgentCoverageSla(report?.agent_coverage_sla)}
${renderOverviewAnalytics(report)}
<section class="dashboard-band">
<div class="band-head"><h3>Рабочая активность сотрудников</h3><span class="muted">загрузка, простои, перегруз и дисциплина процессов</span></div>
@@ -1532,6 +1533,70 @@ function renderAgentQualityNodes(nodes, summary) {
`;
}
function renderAgentCoverageSla(sla) {
const s = sla || {};
const rows = Array.isArray(s.problem_nodes) ? s.problem_nodes.slice(0, 10) : [];
const status = s.sla_status || "UNKNOWN";
return `
<section class="card agent-coverage-card">
<div class="section-head">
<div>
<h3>Покрытие агентов</h3>
<p class="muted">Показывает, насколько KPI репрезентативен по всему парку рабочих мест.</p>
</div>
<span class="badge ${statusClass(status)}">${ui(status)}</span>
</div>
<div class="quality-decision">
<div><span class="muted">Ожидается узлов</span><strong>${escapeHtml(s.expected_nodes ?? 0)}</strong></div>
<div><span class="muted">Данные за 24 часа</span><strong>${escapeHtml(s.reporting_nodes_24h ?? 0)}</strong></div>
<div><span class="muted">Устаревшие</span><strong>${escapeHtml(s.stale_nodes ?? 0)}</strong></div>
<div><span class="muted">Отсутствующие</span><strong>${escapeHtml(s.missing_nodes ?? 0)}</strong></div>
<div><span class="muted">Покрытие</span><strong>${escapeHtml(s.coverage_pct ?? 0)}%</strong></div>
<div><span class="muted">Свежесть</span><strong>${escapeHtml(s.freshness_pct ?? 0)}%</strong></div>
</div>
${status === "CRITICAL" ? `<div class="quality-warning">Покрытие агентов критически недостаточно: KPI не может считаться репрезентативным.</div>` : ""}
${status === "WARNING" ? `<div class="quality-warning">KPI требует проверки: часть рабочих мест не присылает свежую телеметрию.</div>` : ""}
${status === "UNKNOWN" ? `<p class="muted">Список ожидаемых рабочих мест не настроен.</p>` : ""}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Узел</th>
<th>Подразделение</th>
<th>Ответственный</th>
<th>Последняя телеметрия</th>
<th>Статус</th>
<th>Рекомендация</th>
</tr>
</thead>
<tbody>
${rows.length ? rows.map(item => `
<tr>
<td>${ui(item.hostname || "unknown")}</td>
<td>${ui(item.department || "-")}</td>
<td>${ui(item.owner || "-")}</td>
<td>${escapeHtml(item.last_seen_utc || "-")}</td>
<td><span class="badge ${statusClass(item.status)}">${ui(item.status || "UNKNOWN")}</span></td>
<td>${ui(item.recommendation || "")}</td>
</tr>
`).join("") : `
<tr>
<td>${Number(s.expected_nodes || 0) === 0 ? "Нет списка" : "Проблем нет"}</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td><span class="badge ${statusClass(status)}">${ui(status)}</span></td>
<td>${Number(s.expected_nodes || 0) === 0 ? "Настроить expected_nodes.json." : "Действий не требуется."}</td>
</tr>
`}
</tbody>
</table>
</div>
${Array.isArray(s.problem_nodes) && s.problem_nodes.length > 10 ? `<p class="muted small">Показаны первые 10 проблемных узлов из ${escapeHtml(s.problem_nodes.length)}.</p>` : ""}
</section>
`;
}
function renderReports(data) {
data = periodReport(data);
return `
@@ -1569,6 +1634,7 @@ function renderReports(data) {
${renderAgentQuality(data.agent_quality, data.agent_quality_explain)}
${renderAgentQualityHistory(data.agent_quality_history, data.agent_quality_history_summary)}
${renderAgentQualityNodes(data.agent_quality_nodes, data.agent_quality_nodes_summary)}
${renderAgentCoverageSla(data.agent_coverage_sla)}
${renderUebaRisk(data.ueba_risk)}
${renderWorkforceIndexExplanation(data.workforce_policy)}
<h3 class="section-title">Срезы отчета</h3>