From da6e80a90cee07da2f9b834b5a307ff74b2cf229 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Thu, 4 Jun 2026 17:31:50 +0300 Subject: [PATCH] feat(portal): prioritize risk narrative --- adk-rust/crates/detmir-portal/src/main.rs | 284 ++++++++++++------ .../crates/detmir-portal/src/static/app.js | 18 +- docs/BUSINESS_RISK_RU.md | 49 ++- scripts/detmir-portal-tabs-smoke.mjs | 1 + 4 files changed, 239 insertions(+), 113 deletions(-) diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index a367752..b858ca3 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -517,11 +517,13 @@ struct ExecutiveCandidateSummary { #[derive(Clone, Debug, Serialize)] struct ExecutiveDashboardSummary { + #[serde(skip_serializing_if = "Option::is_none")] + risk_narrative_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + main_risk_cause: Option, main_risk: String, main_improvement: String, main_data_gap: String, - #[serde(skip_serializing_if = "Option::is_none")] - main_risk_cause: Option, } #[derive(Clone, Debug, Serialize)] @@ -2378,7 +2380,24 @@ fn build_reports( } else { "Контур 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!( "Достоверность данных агента: {}. {}", @@ -2401,7 +2420,7 @@ fn build_reports( "Открытые вопросы: {}, в работе: {}", metrics.open_incidents, metrics.acknowledged_incidents ), - ]; + ]); if agent_quality_history_summary.ok_days < 5 { 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) { executive_points.push(format!( "Подразделение {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_dashboard.summary.main_improvement @@ -3060,6 +3044,7 @@ fn build_risk_heatmap( critical_candidates, ); let links = risk_narrative_links( + &heat_level, trust_kpi_score, activity_score, agent_coverage_pct, @@ -3068,6 +3053,7 @@ fn build_risk_heatmap( critical_candidates, ); let summary = risk_heatmap_summary( + &heat_level, trust_kpi_score, activity_score, agent_coverage_pct, @@ -3256,6 +3242,7 @@ fn heatmap_rank(level: &str) -> u8 { } fn risk_narrative_links( + heat_level: &str, trust_kpi_score: Option, activity_score: Option, agent_coverage_pct: Option, @@ -3278,6 +3265,16 @@ fn risk_narrative_links( label: "Business Risk".to_string(), 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 { target: "incident_candidates".to_string(), label: "Кандидаты".to_string(), @@ -3297,6 +3294,7 @@ fn risk_narrative_links( } fn risk_heatmap_summary( + heat_level: &str, trust_kpi_score: Option, activity_score: Option, agent_coverage_pct: Option, @@ -3305,10 +3303,11 @@ fn risk_heatmap_summary( critical_candidates: usize, ) -> String { 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(agent_coverage_pct), business_risk_level.unwrap_or("UNKNOWN"), + heat_level, critical_candidates, open_cases, 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 { let mut items = heatmap .iter() @@ -3526,6 +3510,16 @@ fn build_executive_dashboard( let resolved_cases_30d = resolved_cases_30d(inputs.cases); let forensics_readiness = forensics_readiness(snapshot, inputs.candidates, inputs.evidence); 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( &high_risk_departments, &critical_candidates, @@ -3537,11 +3531,6 @@ fn build_executive_dashboard( &critical_candidates, ), 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 { trust_kpi_score, @@ -3673,6 +3662,35 @@ fn executive_main_data_gap( "критичных пробелов в данных не выявлено".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( risk_heatmap: &[RiskHeatmapItem], security_correlation: &[SecurityCorrelationItem], @@ -3684,13 +3702,7 @@ fn executive_main_risk_cause( let correlation = security_correlation .iter() .find(|item| item.department == top.department); - let mut statement = linked_risk_statement(top); - if let Some(item) = correlation.filter(|item| item.correlation_score > 0) { - statement.push_str(&format!( - " Корреляция Workforce/Security: {}/100, {}.", - item.correlation_score, item.correlation_reason - )); - } + let mut statement = risk_narrative_statement(top, correlation); if !matches!(forensics_readiness, "READY") { statement.push_str(&format!( " Готовность Forensics: {forensics_readiness}, доказательная база требует проверки." @@ -3699,6 +3711,38 @@ fn executive_main_risk_cause( 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( snapshot: &Snapshot, candidate_id: &str, @@ -5652,6 +5696,11 @@ fn render_report_markdown( text.push_str(&format!("- {item}\n")); } 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_history_markdown( &mut text, @@ -5672,11 +5721,6 @@ fn render_report_markdown( ); append_risk_heatmap_markdown(&mut text, context.risk_heatmap); 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_incident_review_markdown(&mut text, context.risk_incident_candidates); append_incident_review_audit_markdown( @@ -5692,6 +5736,23 @@ fn render_report_markdown( fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveDashboard) { 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!( "- Trust KPI: {}\n", dashboard @@ -5737,14 +5798,6 @@ fn append_executive_dashboard_markdown(text: &mut String, dashboard: &ExecutiveD .as_deref() .unwrap_or("UNKNOWN") )); - text.push_str(&format!( - "- Главная причина риска: {}\n", - dashboard - .summary - .main_risk_cause - .as_deref() - .unwrap_or("связанный риск не выражен") - )); text.push_str(&format!( "- Главный риск: {}\n", dashboard.summary.main_risk @@ -6043,14 +6096,15 @@ fn append_linked_risk_narrative_markdown( .map(|value| format!("{}/100", value.correlation_score)) .unwrap_or_else(|| "0/100".to_string()); 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, 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_text, item.critical_candidates.unwrap_or(0), item.open_cases.unwrap_or(0), - correlation_text, item.summary.as_deref().unwrap_or("вывод не сформирован") )); if let Some(value) = correlation.and_then(|value| value.explanation.as_deref()) { @@ -9755,6 +9809,27 @@ mod tests { .unwrap() .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"][0]["department"], "Бухгалтерия"); 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]["activity_score"], 50); 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::>(); + 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!( report["risk_heatmap"][0]["summary"] .as_str() .unwrap() - .contains("Trust") + .contains("Security Correlation") ); assert!(report["security_correlation"].is_array()); assert_eq!( @@ -9967,21 +10062,21 @@ mod tests { .as_array() .unwrap() .iter() - .any(|item| item.as_str().unwrap().contains("причина:")) + .next() + .unwrap() + .as_str() + .unwrap() + .contains("Главный управленческий вывод") ); assert!( report["executive_points"] .as_array() .unwrap() .iter() - .any(|item| item.as_str().unwrap().contains("Связанная картина риска")) - ); - assert!( - report["executive_points"] - .as_array() - .unwrap() - .iter() - .any(|item| item.as_str().unwrap().contains("Кандидат в инцидент")) + .any(|item| item + .as_str() + .unwrap() + .contains("Статус связанной картины риска")) ); assert!( report["markdown"] @@ -10007,6 +10102,11 @@ mod tests { .unwrap() .contains("## Связанная картина риска") ); + let markdown = report["markdown"].as_str().unwrap(); + assert!( + markdown.find("## Связанная картина риска").unwrap() + < markdown.find("## Достоверность данных").unwrap() + ); assert!( report["markdown"] .as_str() diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index 0fc9460..956b2f5 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -23,9 +23,9 @@ async function postJson(path, payload) { function statusClass(status) { const s = String(status || "UNKNOWN").toLowerCase(); - if (s === "ok" || s === "true" || 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 === "degraded" || s === "high" || s === "confirmed" || s === "rejected" || s === "archived") return "status-degraded"; + if (s === "ok" || s === "true" || s === "normal" || s === "low" || s === "false_positive" || s === "resolved") return "status-ok"; + 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 === "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"; return "status-unknown"; } @@ -319,6 +319,7 @@ function renderExecutiveDashboard(report) { const highRisk = Array.isArray(dashboard.high_risk_departments) ? dashboard.high_risk_departments : []; const candidates = Array.isArray(dashboard.critical_candidates) ? dashboard.critical_candidates : []; const summary = dashboard.summary || {}; + const narrativeStatus = summary.risk_narrative_status || executiveDashboardStatus(dashboard); return `
@@ -326,7 +327,11 @@ function renderExecutiveDashboard(report) {

Сводка руководителя

Что происходит в организации прямо сейчас: доверие к KPI, покрытие, риски, дела и готовность расследований.

- ${ui(dashboard.forensics_readiness || "UNKNOWN")} + ${ui(narrativeStatus)} + +
+
Главная причина риска${ui(summary.main_risk_cause || "связанный риск не выражен")}${ui(narrativeStatus)}
+
Подтверждающие слоиTrust KPI · Agent Coverage · Business Risk · Risk Heatmap · Security Correlation · Incident Candidates · Cases
Trust KPI${ui(optionalPercent(dashboard.trust_kpi_score))}
@@ -337,13 +342,10 @@ function renderExecutiveDashboard(report) {
Закрыто за 30 дней${ui(dashboard.resolved_cases_30d ?? 0)}
-
Главная причина риска${ui(summary.main_risk_cause || "связанный риск не выражен")}
Главный риск${ui(summary.main_risk || "нет данных")}
Главное улучшение${ui(summary.main_improvement || "нет данных")}
Пробел в данных${ui(summary.main_data_gap || "нет данных")}
- ${highRisk.length ? `

Риск-подразделения: ${ui(highRisk.slice(0, 5).map(item => `${item.department} (${item.risk_level})`).join("; "))}

` : ""} - ${candidates.length ? `

Кандидаты для проверки: ${ui(candidates.slice(0, 5).map(item => `${item.id} (${item.risk_level})`).join("; "))}

` : ""}
`; } @@ -1838,7 +1840,9 @@ function renderRiskLayerLinks(links) { function riskLayerTarget(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 === "security_correlation") return { tab: "operator", selector: ".security-correlation-card" }; 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" }; diff --git a/docs/BUSINESS_RISK_RU.md b/docs/BUSINESS_RISK_RU.md index 626a02d..e5bd8c3 100644 --- a/docs/BUSINESS_RISK_RU.md +++ b/docs/BUSINESS_RISK_RU.md @@ -1,8 +1,13 @@ # 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 не является автоматическим обвинением сотрудников или -подразделений. Это read-only приоритизация управленческой проверки. +Business Risk и Risk Narrative не являются автоматическим обвинением +сотрудников или подразделений. Это read-only приоритизация управленческой +проверки. ## API @@ -62,9 +68,11 @@ Business Risk не является автоматическим обвинен - `summary.main_risk` - главный риск текущего среза; - `summary.main_improvement` - главное подтвержденное улучшение; - `summary.main_data_gap` - главный пробел в данных. +- `summary.risk_narrative_status` - optional единый статус связанной картины + риска: `NORMAL`, `ATTENTION`, `HIGH_RISK` или `CRITICAL`; - `summary.main_risk_cause` - optional связанная причина риска: - Trust KPI, coverage, Business Risk, candidates, cases и готовность - Forensics. + Trust KPI, Agent Coverage, Risk Heatmap, Business Risk, Security + Correlation, Incident Candidates, Cases и готовность Forensics. `risk_heatmap`: @@ -79,9 +87,11 @@ Business Risk не является автоматическим обвинен - `heat_level` - итоговая зона карты: `LOW`, `MEDIUM`, `HIGH`, `CRITICAL` или `UNKNOWN`. - `links` - optional read-only переходы к уже существующим слоям портала: - Trust KPI, Business Risk, кандидаты, дела и покрытие агентов. + Trust KPI, Agent Coverage, Business Risk, Risk Heatmap, Security + Correlation, Incident Candidates и Cases. - `summary` - optional связанная строка - `Trust → Coverage → Business Risk → Candidates → Cases → вывод`. + `Trust → Coverage → Business Risk → Risk Heatmap → Security Correlation → + Candidates → Cases → вывод`. `security_correlation`: @@ -336,13 +346,19 @@ POST /api/cases/{case_id}/status ## Markdown -Оперативный отчет содержит раздел: +Оперативный отчет содержит раздел `Связанная картина риска` сразу после +`Сводка руководителя`, до технических деталей. Это главный управленческий +вывод: что происходит, почему это риск и какие слои это подтверждают. + +Оперативный отчет содержит разделы: ```text ## Сводка руководителя +## Связанная картина риска +## Достоверность данных +## SLA покрытия агентов ## Карта рисков подразделений ## Корреляция Workforce ↔ Security -## Связанная картина риска ## Риски подразделений ## Динамика бизнес-рисков ## Кандидаты в инциденты @@ -354,13 +370,18 @@ POST /api/cases/{case_id}/status ## Executive Summary -Executive summary показывает до 3 наиболее рискованных подразделений, если -их уровень риска выше `LOW`. +Executive summary сначала показывает связанный управленческий вывод по Risk +Narrative, а затем только поддерживающие технические пункты. Отдельные строки +по Business Risk и кандидатам не дублируют главный вывод, если эти причины уже +вошли в narrative. Формат: ```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`, diff --git a/scripts/detmir-portal-tabs-smoke.mjs b/scripts/detmir-portal-tabs-smoke.mjs index 88ef1ca..cc1ec92 100644 --- a/scripts/detmir-portal-tabs-smoke.mjs +++ b/scripts/detmir-portal-tabs-smoke.mjs @@ -127,6 +127,7 @@ async function main() { "Сотрудников в работе", "Средний индекс активности", "Главная причина риска", + "Подтверждающие слои", "Достоверность данных агента", "Стабильность агента за 7 дней", "Качество данных по рабочим местам",