feat(portal): add executive action center

This commit is contained in:
igor04091968
2026-06-07 16:39:49 +03:00
parent 307170b128
commit 2b41fb14c7
11 changed files with 1069 additions and 3 deletions
@@ -988,6 +988,83 @@ h1 {
margin: 12px 0;
}
.action-center-card {
margin: 12px 0;
}
.security-actions-card {
border-left: 4px solid #7c2d12;
}
.action-center-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-top: 12px;
}
.action-item {
display: grid;
gap: 8px;
min-height: 196px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
}
.action-item-head {
display: flex;
align-items: flex-start;
gap: 8px;
}
.action-item-head strong,
.action-item p,
.action-item li {
overflow-wrap: anywhere;
}
.action-item p {
margin: 0;
}
.action-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
color: var(--muted);
font-size: 13px;
}
.action-meta span {
padding: 8px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.action-reasons {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.action-reasons span {
padding: 4px 7px;
border-radius: 999px;
background: var(--panel);
color: var(--muted);
font-size: 12px;
}
.action-item ul {
display: grid;
gap: 6px;
margin: 0;
padding-left: 18px;
}
.risk-narrative-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1303,6 +1380,8 @@ pre {
.executive-grid { grid-template-columns: 1fr; }
.risk-narrative-grid { grid-template-columns: 1fr; }
.risk-narrative-columns { grid-template-columns: 1fr; }
.action-center-list { grid-template-columns: 1fr; }
.action-meta { grid-template-columns: 1fr; }
.analytics-grid { grid-template-columns: 1fr; }
.incident-row { grid-template-columns: 1fr; }
.evidence-row { grid-template-columns: 1fr; }
@@ -738,6 +738,64 @@ function renderRiskNarrative(report) {
`;
}
function renderActionCenter(actions, options = {}) {
const items = Array.isArray(actions) ? actions.slice(0, 6) : [];
if (!items.length) {
return `
<section class="card action-center-card">
<div class="section-head">
<div>
<h3>${ui(options.title || "Рекомендуемые действия")}</h3>
<p class="muted">Критичных действий по текущему срезу не требуется.</p>
</div>
<span class="badge status-ok">low</span>
</div>
</section>
`;
}
return `
<section class="card action-center-card ${options.security ? "security-actions-card" : ""}">
<div class="section-head">
<div>
<h3 ${tooltip("Rule-based список действий: что сделать, кому адресовано, срок и почему система предлагает это действие.")}>${ui(options.title || "Рекомендуемые действия")}</h3>
<p class="muted">Рекомендации сформированы по Workforce KPI, UEBA, покрытию данных, корреляции безопасности и кандидатам на проверку.</p>
</div>
<span class="badge ${statusClass(items[0]?.priority)}">${ui(items[0]?.priority || "low")}</span>
</div>
<div class="action-center-list">
${items.map(action => `
<article class="action-item">
<div class="action-item-head">
<span class="badge ${statusClass(action.priority)}">${ui(action.priority || "low")}</span>
<strong>${ui(action.title || "Действие не указано")}</strong>
</div>
<p>${ui(action.summary || "Описание действия не указано")}</p>
<div class="action-meta">
<span><strong>Срок</strong> ${ui(action.recommended_deadline || "72h")}</span>
<span><strong>Адресат</strong> ${ui(actionOwnerLabel(action.owner_role))}</span>
</div>
<div class="action-reasons">
${(Array.isArray(action.reason_codes) ? action.reason_codes : []).slice(0, 4).map(code => `<span>${ui(code)}</span>`).join("")}
</div>
<ul>${(Array.isArray(action.evidence) ? action.evidence : []).slice(0, 3).map(item => `<li>${ui(item)}</li>`).join("")}</ul>
</article>
`).join("")}
</div>
</section>
`;
}
function actionOwnerLabel(value) {
const map = {
executive: "Руководитель",
manager: "Руководитель подразделения",
security: "ИБ",
forensics: "Расследования",
admin: "Эксплуатация"
};
return map[value] || value || "Ответственный";
}
function optionalPercent(value) {
const number = Number(value);
return Number.isFinite(number) ? `${Math.round(number)}%` : "нет данных";
@@ -1289,6 +1347,7 @@ function renderOperatorRoleContent(mode, data, report, extras = {}) {
function renderExecutiveView(report, incidents) {
return `
${renderRiskNarrative(report)}
${renderActionCenter(report?.recommended_actions, { title: "Рекомендуемые действия" })}
${renderExecutiveDashboard(report)}
${renderKpiExplain(report?.workforce_kpi_explain)}
${renderSecurityEventsSummary(report?.security_events_summary, { compact: true })}
@@ -1301,6 +1360,7 @@ function renderExecutiveView(report, incidents) {
function renderSecurityView(data, report, extras = {}) {
const cases = Array.isArray(extras.cases?.cases) ? extras.cases.cases : [];
return `
${renderActionCenter(report?.recommended_actions, { title: "Рекомендуемые действия ИБ", security: true })}
${renderSecurityEventsSummary(report?.security_events_summary)}
${renderRiskIncidentCandidates(report?.risk_incident_candidates)}
${renderSecurityCorrelation(report?.security_correlation)}
@@ -2933,6 +2993,7 @@ function renderReports(data) {
</div>
${renderPeriodBanner(data)}
${renderRiskNarrative(data)}
${renderActionCenter(data.recommended_actions, { title: "Рекомендуемые действия" })}
${renderExecutiveDashboard(data)}
${renderReportTypeCards()}
<div class="grid-2">