feat(portal): prioritize risk narrative

This commit is contained in:
igor04091968
2026-06-04 17:31:50 +03:00
parent f60cca7f59
commit da6e80a90c
4 changed files with 239 additions and 113 deletions
+191 -91
View File
@@ -517,11 +517,13 @@ struct ExecutiveCandidateSummary {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
struct ExecutiveDashboardSummary { struct ExecutiveDashboardSummary {
#[serde(skip_serializing_if = "Option::is_none")]
risk_narrative_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
main_risk_cause: Option<String>,
main_risk: String, main_risk: String,
main_improvement: String, main_improvement: String,
main_data_gap: String, main_data_gap: String,
#[serde(skip_serializing_if = "Option::is_none")]
main_risk_cause: Option<String>,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@@ -2378,7 +2380,24 @@ fn build_reports(
} else { } else {
"Контур DetMir требует технической проверки" "Контур DetMir требует технической проверки"
}; };
let mut executive_points = vec![ let mut executive_points = Vec::new();
executive_points.push(format!(
"Главный управленческий вывод: {}",
executive_dashboard
.summary
.main_risk_cause
.as_deref()
.unwrap_or("связанный риск не выражен")
));
executive_points.push(format!(
"Статус связанной картины риска: {}",
executive_dashboard
.summary
.risk_narrative_status
.as_deref()
.unwrap_or("NORMAL")
));
executive_points.extend([
format!("Сбор данных: {}. {}", collection.status, collection.text), format!("Сбор данных: {}. {}", collection.status, collection.text),
format!( format!(
"Достоверность данных агента: {}. {}", "Достоверность данных агента: {}. {}",
@@ -2401,7 +2420,7 @@ fn build_reports(
"Открытые вопросы: {}, в работе: {}", "Открытые вопросы: {}, в работе: {}",
metrics.open_incidents, metrics.acknowledged_incidents metrics.open_incidents, metrics.acknowledged_incidents
), ),
]; ]);
if agent_quality_history_summary.ok_days < 5 { if agent_quality_history_summary.ok_days < 5 {
executive_points.push("KPI требует валидации: нестабильный сбор данных агента".to_string()); executive_points.push("KPI требует валидации: нестабильный сбор данных агента".to_string());
} }
@@ -2421,46 +2440,11 @@ fn build_reports(
), ),
_ => {} _ => {}
} }
for item in business_risk.iter().take(3) {
if item.risk_level != "LOW" {
let reason = item
.reasons
.first()
.cloned()
.unwrap_or_else(|| "требуется управленческая проверка".to_string());
executive_points.push(format!(
"Подразделение {}: {} — причина: {}",
item.department, item.risk_level, reason
));
}
}
for department in stable_high_risk_departments(&business_risk_history, 3) { for department in stable_high_risk_departments(&business_risk_history, 3) {
executive_points.push(format!( executive_points.push(format!(
"Подразделение {department} сохраняет высокий риск несколько дней подряд." "Подразделение {department} сохраняет высокий риск несколько дней подряд."
)); ));
} }
for candidate in risk_incident_candidates.iter().take(3) {
executive_points.push(format!(
"Кандидат в инцидент {}: {} — {}",
candidate.id,
candidate.risk_level.as_deref().unwrap_or("UNKNOWN"),
candidate.reason.as_deref().unwrap_or("требуется проверка")
));
}
for item in risk_heatmap
.iter()
.filter(|item| !matches!(item.heat_level.as_str(), "LOW" | "UNKNOWN"))
.take(3)
{
executive_points.push(format!(
"Связанная картина риска: {}",
linked_risk_statement(item)
));
}
executive_points.push(format!(
"Главный риск: {}",
executive_dashboard.summary.main_risk
));
executive_points.push(format!( executive_points.push(format!(
"Главное улучшение: {}", "Главное улучшение: {}",
executive_dashboard.summary.main_improvement executive_dashboard.summary.main_improvement
@@ -3060,6 +3044,7 @@ fn build_risk_heatmap(
critical_candidates, critical_candidates,
); );
let links = risk_narrative_links( let links = risk_narrative_links(
&heat_level,
trust_kpi_score, trust_kpi_score,
activity_score, activity_score,
agent_coverage_pct, agent_coverage_pct,
@@ -3068,6 +3053,7 @@ fn build_risk_heatmap(
critical_candidates, critical_candidates,
); );
let summary = risk_heatmap_summary( let summary = risk_heatmap_summary(
&heat_level,
trust_kpi_score, trust_kpi_score,
activity_score, activity_score,
agent_coverage_pct, agent_coverage_pct,
@@ -3256,6 +3242,7 @@ fn heatmap_rank(level: &str) -> u8 {
} }
fn risk_narrative_links( fn risk_narrative_links(
heat_level: &str,
trust_kpi_score: Option<u8>, trust_kpi_score: Option<u8>,
activity_score: Option<u8>, activity_score: Option<u8>,
agent_coverage_pct: Option<u8>, agent_coverage_pct: Option<u8>,
@@ -3278,6 +3265,16 @@ fn risk_narrative_links(
label: "Business Risk".to_string(), label: "Business Risk".to_string(),
summary: format!("уровень {}", business_risk_level.unwrap_or("UNKNOWN")), summary: format!("уровень {}", business_risk_level.unwrap_or("UNKNOWN")),
}, },
RiskNarrativeLink {
target: "risk_heatmap".to_string(),
label: "Карта риска".to_string(),
summary: format!("итоговый статус карты: {heat_level}"),
},
RiskNarrativeLink {
target: "security_correlation".to_string(),
label: "Корреляция".to_string(),
summary: "связь Workforce и Security по подразделению".to_string(),
},
RiskNarrativeLink { RiskNarrativeLink {
target: "incident_candidates".to_string(), target: "incident_candidates".to_string(),
label: "Кандидаты".to_string(), label: "Кандидаты".to_string(),
@@ -3297,6 +3294,7 @@ fn risk_narrative_links(
} }
fn risk_heatmap_summary( fn risk_heatmap_summary(
heat_level: &str,
trust_kpi_score: Option<u8>, trust_kpi_score: Option<u8>,
activity_score: Option<u8>, activity_score: Option<u8>,
agent_coverage_pct: Option<u8>, agent_coverage_pct: Option<u8>,
@@ -3305,10 +3303,11 @@ fn risk_heatmap_summary(
critical_candidates: usize, critical_candidates: usize,
) -> String { ) -> String {
format!( format!(
"Trust {} → Coverage {} → Business Risk {} → Candidates {} → Cases {} → {}", "Trust {} → Coverage {} → Business Risk {} → Risk Heatmap {} → Security Correlation → Candidates {} → Cases {} → {}",
optional_score_text(trust_kpi_score), optional_score_text(trust_kpi_score),
optional_score_text(agent_coverage_pct), optional_score_text(agent_coverage_pct),
business_risk_level.unwrap_or("UNKNOWN"), business_risk_level.unwrap_or("UNKNOWN"),
heat_level,
critical_candidates, critical_candidates,
open_cases, open_cases,
risk_narrative_conclusion( risk_narrative_conclusion(
@@ -3359,21 +3358,6 @@ fn risk_narrative_conclusion(
} }
} }
fn linked_risk_statement(item: &RiskHeatmapItem) -> String {
format!(
"В подразделении {} {}.",
item.department,
risk_narrative_conclusion(
item.trust_kpi_score,
item.activity_score,
item.agent_coverage_pct,
item.business_risk_level.as_deref(),
item.open_cases.unwrap_or(0),
item.critical_candidates.unwrap_or(0),
)
)
}
fn build_security_correlation(heatmap: &[RiskHeatmapItem]) -> Vec<SecurityCorrelationItem> { fn build_security_correlation(heatmap: &[RiskHeatmapItem]) -> Vec<SecurityCorrelationItem> {
let mut items = heatmap let mut items = heatmap
.iter() .iter()
@@ -3526,6 +3510,16 @@ fn build_executive_dashboard(
let resolved_cases_30d = resolved_cases_30d(inputs.cases); let resolved_cases_30d = resolved_cases_30d(inputs.cases);
let forensics_readiness = forensics_readiness(snapshot, inputs.candidates, inputs.evidence); let forensics_readiness = forensics_readiness(snapshot, inputs.candidates, inputs.evidence);
let summary = ExecutiveDashboardSummary { let summary = ExecutiveDashboardSummary {
risk_narrative_status: Some(risk_narrative_status(
inputs.risk_heatmap,
inputs.security_correlation,
&forensics_readiness,
)),
main_risk_cause: executive_main_risk_cause(
inputs.risk_heatmap,
inputs.security_correlation,
&forensics_readiness,
),
main_risk: executive_main_risk( main_risk: executive_main_risk(
&high_risk_departments, &high_risk_departments,
&critical_candidates, &critical_candidates,
@@ -3537,11 +3531,6 @@ fn build_executive_dashboard(
&critical_candidates, &critical_candidates,
), ),
main_data_gap: executive_main_data_gap(snapshot, inputs.agent_quality_explain), main_data_gap: executive_main_data_gap(snapshot, inputs.agent_quality_explain),
main_risk_cause: executive_main_risk_cause(
inputs.risk_heatmap,
inputs.security_correlation,
&forensics_readiness,
),
}; };
ExecutiveDashboard { ExecutiveDashboard {
trust_kpi_score, trust_kpi_score,
@@ -3673,6 +3662,35 @@ fn executive_main_data_gap(
"критичных пробелов в данных не выявлено".to_string() "критичных пробелов в данных не выявлено".to_string()
} }
fn risk_narrative_status(
risk_heatmap: &[RiskHeatmapItem],
security_correlation: &[SecurityCorrelationItem],
forensics_readiness: &str,
) -> String {
let worst_heat_rank = risk_heatmap
.iter()
.map(|item| heatmap_rank(item.heat_level.as_str()))
.max()
.unwrap_or_else(|| heatmap_rank("UNKNOWN"));
let max_correlation = security_correlation
.iter()
.map(|item| item.correlation_score)
.max()
.unwrap_or(0);
if worst_heat_rank >= heatmap_rank("CRITICAL") || max_correlation >= 85 {
"CRITICAL".to_string()
} else if worst_heat_rank >= heatmap_rank("HIGH") || max_correlation >= 60 {
"HIGH_RISK".to_string()
} else if worst_heat_rank >= heatmap_rank("MEDIUM")
|| max_correlation > 0
|| !matches!(forensics_readiness, "READY" | "OBSERVE")
{
"ATTENTION".to_string()
} else {
"NORMAL".to_string()
}
}
fn executive_main_risk_cause( fn executive_main_risk_cause(
risk_heatmap: &[RiskHeatmapItem], risk_heatmap: &[RiskHeatmapItem],
security_correlation: &[SecurityCorrelationItem], security_correlation: &[SecurityCorrelationItem],
@@ -3684,13 +3702,7 @@ fn executive_main_risk_cause(
let correlation = security_correlation let correlation = security_correlation
.iter() .iter()
.find(|item| item.department == top.department); .find(|item| item.department == top.department);
let mut statement = linked_risk_statement(top); let mut statement = risk_narrative_statement(top, correlation);
if let Some(item) = correlation.filter(|item| item.correlation_score > 0) {
statement.push_str(&format!(
" Корреляция Workforce/Security: {}/100, {}.",
item.correlation_score, item.correlation_reason
));
}
if !matches!(forensics_readiness, "READY") { if !matches!(forensics_readiness, "READY") {
statement.push_str(&format!( statement.push_str(&format!(
" Готовность Forensics: {forensics_readiness}, доказательная база требует проверки." " Готовность Forensics: {forensics_readiness}, доказательная база требует проверки."
@@ -3699,6 +3711,38 @@ fn executive_main_risk_cause(
Some(statement) Some(statement)
} }
fn risk_narrative_statement(
item: &RiskHeatmapItem,
correlation: Option<&SecurityCorrelationItem>,
) -> String {
let correlation_score = correlation
.map(|value| format!("{}/100", value.correlation_score))
.unwrap_or_else(|| "0/100".to_string());
let correlation_reason = correlation
.map(|value| format!(" Причина корреляции: {}.", value.correlation_reason))
.unwrap_or_default();
format!(
"В подразделении {} связаны слои: Trust KPI {}, Agent Coverage {}, Business Risk {}, Risk Heatmap {}, Security Correlation {}, Incident Candidates {}, Cases {}. {}.{}",
item.department,
optional_score_text(item.trust_kpi_score),
optional_score_text(item.agent_coverage_pct),
item.business_risk_level.as_deref().unwrap_or("UNKNOWN"),
item.heat_level,
correlation_score,
item.critical_candidates.unwrap_or(0),
item.open_cases.unwrap_or(0),
risk_narrative_conclusion(
item.trust_kpi_score,
item.activity_score,
item.agent_coverage_pct,
item.business_risk_level.as_deref(),
item.open_cases.unwrap_or(0),
item.critical_candidates.unwrap_or(0),
),
correlation_reason
)
}
fn build_investigation_pack( fn build_investigation_pack(
snapshot: &Snapshot, snapshot: &Snapshot,
candidate_id: &str, candidate_id: &str,
@@ -5652,6 +5696,11 @@ fn render_report_markdown(
text.push_str(&format!("- {item}\n")); text.push_str(&format!("- {item}\n"));
} }
append_executive_dashboard_markdown(&mut text, context.executive_dashboard); append_executive_dashboard_markdown(&mut text, context.executive_dashboard);
append_linked_risk_narrative_markdown(
&mut text,
context.risk_heatmap,
context.security_correlation,
);
append_agent_quality_markdown(&mut text, &snapshot.agent_quality); append_agent_quality_markdown(&mut text, &snapshot.agent_quality);
append_agent_quality_history_markdown( append_agent_quality_history_markdown(
&mut text, &mut text,
@@ -5672,11 +5721,6 @@ fn render_report_markdown(
); );
append_risk_heatmap_markdown(&mut text, context.risk_heatmap); append_risk_heatmap_markdown(&mut text, context.risk_heatmap);
append_security_correlation_markdown(&mut text, context.security_correlation); append_security_correlation_markdown(&mut text, context.security_correlation);
append_linked_risk_narrative_markdown(
&mut text,
context.risk_heatmap,
context.security_correlation,
);
append_risk_incident_candidates_markdown(&mut text, context.risk_incident_candidates); append_risk_incident_candidates_markdown(&mut text, context.risk_incident_candidates);
append_incident_review_markdown(&mut text, context.risk_incident_candidates); append_incident_review_markdown(&mut text, context.risk_incident_candidates);
append_incident_review_audit_markdown( append_incident_review_audit_markdown(
@@ -5692,6 +5736,23 @@ fn render_report_markdown(
fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveDashboard) { fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveDashboard) {
text.push_str("\n## Сводка руководителя\n\n"); text.push_str("\n## Сводка руководителя\n\n");
text.push_str(&format!(
"- Статус связанной картины риска: {}\n",
dashboard
.summary
.risk_narrative_status
.as_deref()
.unwrap_or("NORMAL")
));
text.push_str(&format!(
"- Главная причина риска: {}\n",
dashboard
.summary
.main_risk_cause
.as_deref()
.unwrap_or("связанный риск не выражен")
));
text.push_str("- Подтверждающие слои: Trust KPI, Agent Coverage, Business Risk, Risk Heatmap, Security Correlation, Incident Candidates, Cases\n");
text.push_str(&format!( text.push_str(&format!(
"- Trust KPI: {}\n", "- Trust KPI: {}\n",
dashboard dashboard
@@ -5737,14 +5798,6 @@ fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveD
.as_deref() .as_deref()
.unwrap_or("UNKNOWN") .unwrap_or("UNKNOWN")
)); ));
text.push_str(&format!(
"- Главная причина риска: {}\n",
dashboard
.summary
.main_risk_cause
.as_deref()
.unwrap_or("связанный риск не выражен")
));
text.push_str(&format!( text.push_str(&format!(
"- Главный риск: {}\n", "- Главный риск: {}\n",
dashboard.summary.main_risk dashboard.summary.main_risk
@@ -6043,14 +6096,15 @@ fn append_linked_risk_narrative_markdown(
.map(|value| format!("{}/100", value.correlation_score)) .map(|value| format!("{}/100", value.correlation_score))
.unwrap_or_else(|| "0/100".to_string()); .unwrap_or_else(|| "0/100".to_string());
text.push_str(&format!( text.push_str(&format!(
"- {} → Trust {} → Coverage {} → Business Risk {} → Candidates {} → Cases {} → Correlation {} → {}\n", "- {} → Trust KPI {} → Agent Coverage {} → Business Risk {} → Risk Heatmap {} → Security Correlation {} → Incident Candidates {} → Cases {} → {}\n",
item.department, item.department,
optional_score_text(item.trust_kpi_score), optional_score_text(item.trust_kpi_score),
optional_score_text(item.agent_coverage_pct), optional_score_text(item.agent_coverage_pct),
item.business_risk_level.as_deref().unwrap_or("UNKNOWN"), item.business_risk_level.as_deref().unwrap_or("UNKNOWN"),
item.heat_level,
correlation_text,
item.critical_candidates.unwrap_or(0), item.critical_candidates.unwrap_or(0),
item.open_cases.unwrap_or(0), item.open_cases.unwrap_or(0),
correlation_text,
item.summary.as_deref().unwrap_or("вывод не сформирован") item.summary.as_deref().unwrap_or("вывод не сформирован")
)); ));
if let Some(value) = correlation.and_then(|value| value.explanation.as_deref()) { if let Some(value) = correlation.and_then(|value| value.explanation.as_deref()) {
@@ -9755,6 +9809,27 @@ mod tests {
.unwrap() .unwrap()
.contains("В подразделении") .contains("В подразделении")
); );
assert_eq!(
report["executive_dashboard"]["summary"]["risk_narrative_status"],
"HIGH_RISK"
);
let main_risk_cause = report["executive_dashboard"]["summary"]["main_risk_cause"]
.as_str()
.unwrap();
for layer in [
"Trust KPI",
"Agent Coverage",
"Risk Heatmap",
"Business Risk",
"Security Correlation",
"Incident Candidates",
"Cases",
] {
assert!(
main_risk_cause.contains(layer),
"main_risk_cause must mention {layer}"
);
}
assert_eq!(report["business_risk"].as_array().unwrap().len(), 1); assert_eq!(report["business_risk"].as_array().unwrap().len(), 1);
assert_eq!(report["business_risk"][0]["department"], "Бухгалтерия"); assert_eq!(report["business_risk"][0]["department"], "Бухгалтерия");
assert_eq!(report["business_risk"][0]["risk_level"], "MEDIUM"); assert_eq!(report["business_risk"][0]["risk_level"], "MEDIUM");
@@ -9787,11 +9862,31 @@ mod tests {
assert_eq!(report["risk_heatmap"][0]["trust_kpi_score"], 50); assert_eq!(report["risk_heatmap"][0]["trust_kpi_score"], 50);
assert_eq!(report["risk_heatmap"][0]["activity_score"], 50); assert_eq!(report["risk_heatmap"][0]["activity_score"], 50);
assert!(report["risk_heatmap"][0]["links"].is_array()); assert!(report["risk_heatmap"][0]["links"].is_array());
let heatmap_link_targets = report["risk_heatmap"][0]["links"]
.as_array()
.unwrap()
.iter()
.filter_map(|item| item["target"].as_str())
.collect::<Vec<_>>();
for target in [
"risk_heatmap",
"trust_kpi",
"agent_coverage",
"business_risk",
"security_correlation",
"incident_candidates",
"cases",
] {
assert!(
heatmap_link_targets.contains(&target),
"risk heatmap links must include {target}"
);
}
assert!( assert!(
report["risk_heatmap"][0]["summary"] report["risk_heatmap"][0]["summary"]
.as_str() .as_str()
.unwrap() .unwrap()
.contains("Trust") .contains("Security Correlation")
); );
assert!(report["security_correlation"].is_array()); assert!(report["security_correlation"].is_array());
assert_eq!( assert_eq!(
@@ -9967,21 +10062,21 @@ mod tests {
.as_array() .as_array()
.unwrap() .unwrap()
.iter() .iter()
.any(|item| item.as_str().unwrap().contains("причина:")) .next()
.unwrap()
.as_str()
.unwrap()
.contains("Главный управленческий вывод")
); );
assert!( assert!(
report["executive_points"] report["executive_points"]
.as_array() .as_array()
.unwrap() .unwrap()
.iter() .iter()
.any(|item| item.as_str().unwrap().contains("Связанная картина риска")) .any(|item| item
); .as_str()
assert!(
report["executive_points"]
.as_array()
.unwrap() .unwrap()
.iter() .contains("Статус связанной картины риска"))
.any(|item| item.as_str().unwrap().contains("Кандидат в инцидент"))
); );
assert!( assert!(
report["markdown"] report["markdown"]
@@ -10007,6 +10102,11 @@ mod tests {
.unwrap() .unwrap()
.contains("## Связанная картина риска") .contains("## Связанная картина риска")
); );
let markdown = report["markdown"].as_str().unwrap();
assert!(
markdown.find("## Связанная картина риска").unwrap()
< markdown.find("## Достоверность данных").unwrap()
);
assert!( assert!(
report["markdown"] report["markdown"]
.as_str() .as_str()
@@ -23,9 +23,9 @@ async function postJson(path, payload) {
function statusClass(status) { function statusClass(status) {
const s = String(status || "UNKNOWN").toLowerCase(); const s = String(status || "UNKNOWN").toLowerCase();
if (s === "ok" || s === "true" || s === "low" || s === "false_positive" || s === "resolved") return "status-ok"; if (s === "ok" || s === "true" || s === "normal" || s === "low" || s === "false_positive" || s === "resolved") return "status-ok";
if (s === "warn" || s === "warning" || s === "fallback" || s === "stale" || s === "medium" || s === "in_review" || s === "postponed" || s === "open" || s === "in_progress") return "status-warn"; if (s === "warn" || s === "warning" || s === "attention" || s === "fallback" || s === "stale" || s === "medium" || s === "in_review" || s === "postponed" || s === "open" || s === "in_progress") return "status-warn";
if (s === "degraded" || s === "high" || s === "confirmed" || s === "rejected" || s === "archived") return "status-degraded"; if (s === "degraded" || s === "high" || s === "high_risk" || s === "confirmed" || s === "rejected" || s === "archived") return "status-degraded";
if (s === "fail" || s === "false" || s === "error" || s === "critical" || s === "missing") return "status-fail"; if (s === "fail" || s === "false" || s === "error" || s === "critical" || s === "missing") return "status-fail";
return "status-unknown"; return "status-unknown";
} }
@@ -319,6 +319,7 @@ function renderExecutiveDashboard(report) {
const highRisk = Array.isArray(dashboard.high_risk_departments) ? dashboard.high_risk_departments : []; const highRisk = Array.isArray(dashboard.high_risk_departments) ? dashboard.high_risk_departments : [];
const candidates = Array.isArray(dashboard.critical_candidates) ? dashboard.critical_candidates : []; const candidates = Array.isArray(dashboard.critical_candidates) ? dashboard.critical_candidates : [];
const summary = dashboard.summary || {}; const summary = dashboard.summary || {};
const narrativeStatus = summary.risk_narrative_status || executiveDashboardStatus(dashboard);
return ` return `
<section class="card executive-dashboard-card"> <section class="card executive-dashboard-card">
<div class="section-head"> <div class="section-head">
@@ -326,7 +327,11 @@ function renderExecutiveDashboard(report) {
<h3>Сводка руководителя</h3> <h3>Сводка руководителя</h3>
<p class="muted">Что происходит в организации прямо сейчас: доверие к KPI, покрытие, риски, дела и готовность расследований.</p> <p class="muted">Что происходит в организации прямо сейчас: доверие к KPI, покрытие, риски, дела и готовность расследований.</p>
</div> </div>
<span class="badge ${statusClass(executiveDashboardStatus(dashboard))}">${ui(dashboard.forensics_readiness || "UNKNOWN")}</span> <span class="badge ${statusClass(narrativeStatus)}">${ui(narrativeStatus)}</span>
</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 class="badge ${statusClass(narrativeStatus)}">${ui(narrativeStatus)}</span></div>
<div class="row compact-row"><strong>Подтверждающие слои</strong><span class="muted">Trust KPI · Agent Coverage · Business Risk · Risk Heatmap · Security Correlation · Incident Candidates · Cases</span><span></span></div>
</div> </div>
<div class="quality-grid"> <div class="quality-grid">
<div><span class="muted">Trust KPI</span><strong>${ui(optionalPercent(dashboard.trust_kpi_score))}</strong></div> <div><span class="muted">Trust KPI</span><strong>${ui(optionalPercent(dashboard.trust_kpi_score))}</strong></div>
@@ -337,13 +342,10 @@ function renderExecutiveDashboard(report) {
<div><span class="muted">Закрыто за 30 дней</span><strong>${ui(dashboard.resolved_cases_30d ?? 0)}</strong></div> <div><span class="muted">Закрыто за 30 дней</span><strong>${ui(dashboard.resolved_cases_30d ?? 0)}</strong></div>
</div> </div>
<div class="list compact-list executive-summary-list"> <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_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_improvement || "нет данных")}</span><span></span></div>
<div class="row compact-row"><strong>Пробел в данных</strong><span class="muted">${ui(summary.main_data_gap || "нет данных")}</span><span></span></div> <div class="row compact-row"><strong>Пробел в данных</strong><span class="muted">${ui(summary.main_data_gap || "нет данных")}</span><span></span></div>
</div> </div>
${highRisk.length ? `<p class="muted small">Риск-подразделения: ${ui(highRisk.slice(0, 5).map(item => `${item.department} (${item.risk_level})`).join("; "))}</p>` : ""}
${candidates.length ? `<p class="muted small">Кандидаты для проверки: ${ui(candidates.slice(0, 5).map(item => `${item.id} (${item.risk_level})`).join("; "))}</p>` : ""}
</section> </section>
`; `;
} }
@@ -1838,7 +1840,9 @@ function renderRiskLayerLinks(links) {
function riskLayerTarget(target) { function riskLayerTarget(target) {
const value = String(target || ""); const value = String(target || "");
if (value === "risk_heatmap") return { tab: "operator", selector: ".risk-heatmap-card" };
if (value === "business_risk") return { tab: "operator", selector: "#business-risk-section" }; if (value === "business_risk") return { tab: "operator", selector: "#business-risk-section" };
if (value === "security_correlation") return { tab: "operator", selector: ".security-correlation-card" };
if (value === "incident_candidates") return { tab: "operator", selector: "#risk-candidates-section" }; if (value === "incident_candidates") return { tab: "operator", selector: "#risk-candidates-section" };
if (value === "cases") return { tab: "incidents", selector: "#cases-section" }; if (value === "cases") return { tab: "incidents", selector: "#cases-section" };
if (value === "agent_coverage") return { tab: "operator", selector: "#agent-coverage-section" }; if (value === "agent_coverage") return { tab: "operator", selector: "#agent-coverage-section" };
+35 -14
View File
@@ -1,8 +1,13 @@
# Business Risk # Business Risk
`Business Risk` - управленческий слой AWatch-rus / DetMir, который показывает `Business Risk` - расчетный слой AWatch-rus / DetMir, который показывает не
не просто активность сотрудников, а зоны организационного риска по просто активность сотрудников, а зоны организационного риска по подразделениям.
подразделениям.
Над ним расположен основной управленческий слой `Risk Narrative`: единая
связанная картина риска для руководителя. Он не создает новых сущностей и не
добавляет workflow, а связывает уже рассчитанные слои в один вывод:
`Trust KPI → Agent Coverage → Business Risk → Risk Heatmap → Security
Correlation → Incident Candidates → Cases`.
## Назначение ## Назначение
@@ -14,8 +19,9 @@
- где есть падение тренда; - где есть падение тренда;
- где проблемные рабочие места портят доверие к отчету. - где проблемные рабочие места портят доверие к отчету.
Business Risk не является автоматическим обвинением сотрудников или Business Risk и Risk Narrative не являются автоматическим обвинением
подразделений. Это read-only приоритизация управленческой проверки. сотрудников или подразделений. Это read-only приоритизация управленческой
проверки.
## API ## API
@@ -62,9 +68,11 @@ Business Risk не является автоматическим обвинен
- `summary.main_risk` - главный риск текущего среза; - `summary.main_risk` - главный риск текущего среза;
- `summary.main_improvement` - главное подтвержденное улучшение; - `summary.main_improvement` - главное подтвержденное улучшение;
- `summary.main_data_gap` - главный пробел в данных. - `summary.main_data_gap` - главный пробел в данных.
- `summary.risk_narrative_status` - optional единый статус связанной картины
риска: `NORMAL`, `ATTENTION`, `HIGH_RISK` или `CRITICAL`;
- `summary.main_risk_cause` - optional связанная причина риска: - `summary.main_risk_cause` - optional связанная причина риска:
Trust KPI, coverage, Business Risk, candidates, cases и готовность Trust KPI, Agent Coverage, Risk Heatmap, Business Risk, Security
Forensics. Correlation, Incident Candidates, Cases и готовность Forensics.
`risk_heatmap`: `risk_heatmap`:
@@ -79,9 +87,11 @@ Business Risk не является автоматическим обвинен
- `heat_level` - итоговая зона карты: `LOW`, `MEDIUM`, `HIGH`, - `heat_level` - итоговая зона карты: `LOW`, `MEDIUM`, `HIGH`,
`CRITICAL` или `UNKNOWN`. `CRITICAL` или `UNKNOWN`.
- `links` - optional read-only переходы к уже существующим слоям портала: - `links` - optional read-only переходы к уже существующим слоям портала:
Trust KPI, Business Risk, кандидаты, дела и покрытие агентов. Trust KPI, Agent Coverage, Business Risk, Risk Heatmap, Security
Correlation, Incident Candidates и Cases.
- `summary` - optional связанная строка - `summary` - optional связанная строка
`Trust → Coverage → Business Risk → Candidates → Cases → вывод`. `Trust → Coverage → Business Risk → Risk Heatmap → Security Correlation →
Candidates → Cases → вывод`.
`security_correlation`: `security_correlation`:
@@ -336,13 +346,19 @@ POST /api/cases/{case_id}/status
## Markdown ## Markdown
Оперативный отчет содержит раздел: Оперативный отчет содержит раздел `Связанная картина риска` сразу после
`Сводка руководителя`, до технических деталей. Это главный управленческий
вывод: что происходит, почему это риск и какие слои это подтверждают.
Оперативный отчет содержит разделы:
```text ```text
## Сводка руководителя ## Сводка руководителя
## Связанная картина риска
## Достоверность данных
## SLA покрытия агентов
## Карта рисков подразделений ## Карта рисков подразделений
## Корреляция Workforce ↔ Security ## Корреляция Workforce ↔ Security
## Связанная картина риска
## Риски подразделений ## Риски подразделений
## Динамика бизнес-рисков ## Динамика бизнес-рисков
## Кандидаты в инциденты ## Кандидаты в инциденты
@@ -354,13 +370,18 @@ POST /api/cases/{case_id}/status
## Executive Summary ## Executive Summary
Executive summary показывает до 3 наиболее рискованных подразделений, если Executive summary сначала показывает связанный управленческий вывод по Risk
их уровень риска выше `LOW`. Narrative, а затем только поддерживающие технические пункты. Отдельные строки
по Business Risk и кандидатам не дублируют главный вывод, если эти причины уже
вошли в narrative.
Формат: Формат:
```text ```text
Подразделение Бухгалтерия: HIGH — причина: низкий Trust KPI Главный управленческий вывод: В подразделении Бухгалтерия связаны слои:
Trust KPI 50%, Agent Coverage 80%, Risk Heatmap HIGH, Business Risk MEDIUM,
Security Correlation 40/100, Incident Candidates 1, Cases 0. риск связан из-за
слабого Trust KPI, падения активности, слабого покрытия агентов.
``` ```
Если подразделение 3+ дня подряд находится в `HIGH` или `CRITICAL`, Если подразделение 3+ дня подряд находится в `HIGH` или `CRITICAL`,
+1
View File
@@ -127,6 +127,7 @@ async function main() {
"Сотрудников в работе", "Сотрудников в работе",
"Средний индекс активности", "Средний индекс активности",
"Главная причина риска", "Главная причина риска",
"Подтверждающие слои",
"Достоверность данных агента", "Достоверность данных агента",
"Стабильность агента за 7 дней", "Стабильность агента за 7 дней",
"Качество данных по рабочим местам", "Качество данных по рабочим местам",