feat(portal): connect risk narrative layers

This commit is contained in:
igor04091968
2026-06-04 15:30:46 +03:00
parent d0e502f622
commit f60cca7f59
4 changed files with 374 additions and 55 deletions
@@ -1,4 +1,4 @@
const state = { tab: "operator", period: "today", links: null, readiness: null, reports: null };
const state = { tab: "operator", period: "today", links: null, readiness: null, reports: null, pendingScrollSelector: null };
function apiBase() {
const path = window.location.pathname;
@@ -337,6 +337,7 @@ function renderExecutiveDashboard(report) {
<div><span class="muted">Закрыто за 30 дней</span><strong>${ui(dashboard.resolved_cases_30d ?? 0)}</strong></div>
</div>
<div class="list compact-list executive-summary-list">
<div class="row compact-row"><strong>Главная причина риска</strong><span class="muted">${ui(summary.main_risk_cause || "связанный риск не выражен")}</span><span></span></div>
<div class="row compact-row"><strong>Главный риск</strong><span class="muted">${ui(summary.main_risk || "нет данных")}</span><span></span></div>
<div class="row compact-row"><strong>Главное улучшение</strong><span class="muted">${ui(summary.main_improvement || "нет данных")}</span><span></span></div>
<div class="row compact-row"><strong>Пробел в данных</strong><span class="muted">${ui(summary.main_data_gap || "нет данных")}</span><span></span></div>
@@ -1360,7 +1361,7 @@ function renderIncidents(data) {
function renderCases(cases) {
const rows = Array.isArray(cases) ? cases.slice(0, 20) : [];
return `
<section class="card cases-card">
<section class="card cases-card" id="cases-section">
<div class="section-head">
<div>
<h3>Дела</h3>
@@ -1533,7 +1534,7 @@ function renderAgentQuality(quality, explain) {
const warn = ["warning", "fallback", "degraded", "error"].includes(String(status).toLowerCase());
const accepted = Boolean(e.kpi_accepted);
return `
<section class="card agent-quality-card">
<section class="card agent-quality-card" id="trust-kpi-section">
<div class="section-head">
<div>
<h3>Достоверность данных агента</h3>
@@ -1665,7 +1666,7 @@ function renderAgentCoverageSla(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">
<section class="card agent-coverage-card" id="agent-coverage-section">
<div class="section-head">
<div>
<h3>Покрытие агентов</h3>
@@ -1728,7 +1729,7 @@ function renderBusinessRisk(items) {
const rows = Array.isArray(items) ? items.slice(0, 10) : [];
const worst = rows[0]?.risk_level || "UNKNOWN";
return `
<section class="card business-risk-card">
<section class="card business-risk-card" id="business-risk-section">
<div class="section-head">
<div>
<h3>Риски подразделений</h3>
@@ -1791,6 +1792,7 @@ function renderRiskHeatmap(items) {
<th>Покрытие</th>
<th>Риск</th>
<th>Дела</th>
<th>Связи</th>
</tr>
</thead>
<tbody>
@@ -1805,6 +1807,7 @@ function renderRiskHeatmap(items) {
<span class="muted small">${ui(item.business_risk_level || "UNKNOWN")} · кандидаты ${ui(item.critical_candidates ?? 0)}</span>
</td>
<td>${ui(item.open_cases ?? 0)}</td>
<td>${renderRiskLayerLinks(item.links)}</td>
</tr>
`).join("") : `
<tr>
@@ -1814,6 +1817,7 @@ function renderRiskHeatmap(items) {
<td>UNKNOWN</td>
<td><span class="badge status-unknown">UNKNOWN</span></td>
<td>0</td>
<td>-</td>
</tr>
`}
</tbody>
@@ -1823,6 +1827,24 @@ function renderRiskHeatmap(items) {
`;
}
function renderRiskLayerLinks(links) {
const items = Array.isArray(links) ? links : [];
if (!items.length) return "-";
return `<div class="button-row compact-actions">${items.map(link => {
const target = riskLayerTarget(link.target);
return `<button class="small-button" data-risk-layer-tab="${ui(target.tab)}" data-risk-layer-selector="${ui(target.selector)}" title="${ui(link.summary || link.label || "")}">${ui(link.label || link.target || "слой")}</button>`;
}).join("")}</div>`;
}
function riskLayerTarget(target) {
const value = String(target || "");
if (value === "business_risk") return { tab: "operator", selector: "#business-risk-section" };
if (value === "incident_candidates") return { tab: "operator", selector: "#risk-candidates-section" };
if (value === "cases") return { tab: "incidents", selector: "#cases-section" };
if (value === "agent_coverage") return { tab: "operator", selector: "#agent-coverage-section" };
return { tab: "operator", selector: "#trust-kpi-section" };
}
function riskPercentText(value) {
const number = Number(value);
return Number.isFinite(number) ? `${Math.round(number)}%` : "UNKNOWN";
@@ -1863,7 +1885,7 @@ function renderSecurityCorrelation(items) {
<td><span class="badge ${statusClass(item.business_risk_level)}">${ui(item.business_risk_level || "UNKNOWN")}</span></td>
<td>кандидаты ${ui(item.critical_candidates ?? 0)} · дела ${ui(item.open_cases ?? 0)}</td>
<td><strong>${ui(Number(item.correlation_score || 0))}/100</strong></td>
<td>${ui(item.correlation_reason || "связь не выражена")}</td>
<td>${ui(item.explanation || item.correlation_reason || "связь не выражена")}</td>
</tr>
`).join("") : `
<tr>
@@ -1957,7 +1979,7 @@ function renderRiskIncidentCandidates(items) {
const rows = Array.isArray(items) ? items.slice(0, 10) : [];
const worst = rows[0]?.risk_level || "UNKNOWN";
return `
<section class="card risk-candidates-card">
<section class="card risk-candidates-card" id="risk-candidates-section">
<div class="section-head">
<div>
<h3>Кандидаты в инциденты</h3>
@@ -2254,6 +2276,7 @@ async function refresh() {
content.innerHTML = renderSettings(data);
updateFilters(data);
}
consumePendingScroll();
}
function setTab(tab) {
@@ -2270,6 +2293,16 @@ function applySecurityMode(tab) {
document.body.classList.toggle("security-mode", tab === "owner" || tab === "incidents" || tab === "perimeter");
}
function consumePendingScroll() {
const selector = state.pendingScrollSelector;
if (!selector) return;
state.pendingScrollSelector = null;
window.setTimeout(() => {
const target = document.querySelector(selector);
if (target) target.scrollIntoView({ behavior: "smooth", block: "start" });
}, 0);
}
function showError(error) {
document.getElementById("content").innerHTML = `<pre>${escapeHtml(error.stack || error.message || error)}</pre>`;
}
@@ -2297,6 +2330,19 @@ document.addEventListener("click", event => {
setTab("incidents");
});
document.addEventListener("click", event => {
const button = event.target.closest("[data-risk-layer-tab]");
if (!button) return;
const selector = button.dataset.riskLayerSelector || "";
state.pendingScrollSelector = selector;
const tab = button.dataset.riskLayerTab || state.tab;
if (tab === state.tab) {
consumePendingScroll();
} else {
setTab(tab);
}
});
document.addEventListener("click", event => {
const button = event.target.closest("[data-incident-action]");
if (!button) return;