feat(portal): add rule-based risk narrative

This commit is contained in:
igor04091968
2026-06-07 15:46:38 +03:00
parent 5277097032
commit 8fcb49f2c7
8 changed files with 1369 additions and 8 deletions
@@ -984,6 +984,53 @@ h1 {
margin: 12px 0;
}
.risk-narrative-card {
margin: 12px 0;
}
.risk-narrative-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 12px 0;
}
.risk-narrative-grid > div {
display: grid;
gap: 4px;
min-height: 64px;
padding: 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
}
.risk-narrative-grid strong {
overflow-wrap: anywhere;
}
.risk-narrative-columns {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.risk-narrative-columns h4 {
margin: 0 0 6px;
font-size: 14px;
}
.risk-narrative-columns ul {
display: grid;
gap: 6px;
margin: 0;
padding-left: 18px;
}
.risk-narrative-columns li {
overflow-wrap: anywhere;
}
.ueba-risk-card {
margin: 12px 0;
}
@@ -1226,6 +1273,7 @@ pre {
.anomaly-panel,
.risk-panel { grid-column: auto; }
.leader-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.risk-narrative-columns { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
@@ -1253,6 +1301,8 @@ pre {
.band-head { flex-direction: column; align-items: flex-start; }
.row { grid-template-columns: 1fr; }
.executive-grid { grid-template-columns: 1fr; }
.risk-narrative-grid { grid-template-columns: 1fr; }
.risk-narrative-columns { grid-template-columns: 1fr; }
.analytics-grid { grid-template-columns: 1fr; }
.incident-row { grid-template-columns: 1fr; }
.evidence-row { grid-template-columns: 1fr; }
@@ -671,6 +671,53 @@ function renderExecutiveDashboard(report) {
}
function renderRiskNarrative(report) {
const narrative = report?.risk_narrative;
if (narrative) {
const why = Array.isArray(narrative.why) ? narrative.why.slice(0, 5) : [];
const evidence = Array.isArray(narrative.evidence) ? narrative.evidence.slice(0, 6) : [];
const actions = Array.isArray(narrative.recommended_actions) ? narrative.recommended_actions.slice(0, 4) : [];
const limitations = Array.isArray(narrative.limitations) ? narrative.limitations.slice(0, 3) : [];
return `
<section class="card risk-narrative-card">
<div class="section-head">
<div>
<h3 ${tooltip("Управленческий вывод: что происходит, насколько это рискованно, почему система так считает и что делать дальше.")}>Риск-нарратив</h3>
<p class="muted">${ui(narrative.summary || "Система связывает показатели активности, полноту данных, оценку риска и очередь проверки.")}</p>
</div>
<span class="badge ${statusClass(narrative.risk_level)}">${ui(narrative.risk_level || "low")} · ${ui(narrative.risk_score ?? 0)}/100</span>
</div>
<div class="risk-narrative-grid">
<div>
<span class="muted">Главный вывод</span>
<strong>${ui(narrative.title || "Риск не рассчитан")}</strong>
</div>
<div>
<span class="muted">Модель</span>
<strong>${ui(narrative.model?.type || "rule_based")}</strong>
</div>
<div>
<span class="muted">Уровень</span>
<strong>${ui(narrative.risk_level || "low")}</strong>
</div>
</div>
<div class="risk-narrative-columns">
<div>
<h4>Почему</h4>
<ul>${(why.length ? why : ["Существенных негативных признаков не выявлено"]).map(item => `<li>${ui(item)}</li>`).join("")}</ul>
</div>
<div>
<h4>Подтверждения</h4>
<ul>${evidence.map(item => `<li><strong>${ui(item.label || item.source || "-")}</strong>: ${ui(item.value || "-")} <span class="badge ${statusClass(item.severity)}">${ui(item.severity || "low")}</span></li>`).join("") || "<li>Подтверждающие сигналы пока не рассчитаны</li>"}</ul>
</div>
<div>
<h4>Дальше</h4>
<ul>${actions.map(item => `<li>${ui(item)}</li>`).join("") || "<li>Продолжить наблюдение</li>"}</ul>
</div>
</div>
${limitations.length ? `<p class="muted small">Ограничения: ${limitations.map(ui).join(" · ")}</p>` : ""}
</section>
`;
}
const dashboard = report?.executive_dashboard;
const summary = dashboard?.summary || {};
const narrativeStatus = summary.risk_narrative_status || executiveDashboardStatus(dashboard || {});