diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index 4bd03c1..673b582 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -2529,16 +2529,17 @@ fn security_events_summary_status(summary: &SecurityEventsSummary) -> String { fn security_events_summary_text(summary: &SecurityEventsSummary) -> String { if summary.backend == "disabled" || summary.status == "disabled" { - return "агрегированные события безопасности отключены".to_string(); + return "Источник событий безопасности отключён. Используется локальный режим без ClickHouse." + .to_string(); } if summary.fallback_used { return format!( - "ClickHouse недоступен, используется локальный режим; причина: {}", + "События безопасности временно недоступны. Используется локальный режим без ClickHouse; причина: {}", summary.error.as_deref().unwrap_or("неизвестная ошибка") ); } format!( - "событий={}, неуспешных входов={}, подозрительных входов={}, RDP={}, изменения учетных записей={}, ошибки агентов={}", + "События безопасности доступны: событий={}, неуспешных входов={}, подозрительных входов={}, RDP={}, изменения учетных записей={}, ошибки агентов={}", summary.events_24h, summary.failed_logins_24h, summary.suspicious_logins_24h, @@ -10135,6 +10136,46 @@ mod tests { ); } + #[test] + fn security_events_summary_texts_are_pilot_safe() { + let disabled = SecurityEventsSummary::disabled(); + assert!( + security_events_summary_text(&disabled) + .contains("Источник событий безопасности отключён") + ); + assert!( + security_events_summary_text(&disabled) + .contains("Используется локальный режим без ClickHouse") + ); + + let fallback = SecurityEventsSummary::fallback("network unavailable", 1); + assert!( + security_events_summary_text(&fallback) + .contains("События безопасности временно недоступны") + ); + assert!( + security_events_summary_text(&fallback) + .contains("Используется локальный режим без ClickHouse") + ); + + let available = SecurityEventsSummary { + status: "ok".to_string(), + backend: "clickhouse".to_string(), + events_24h: 3, + failed_logins_24h: 1, + suspicious_logins_24h: 0, + rdp_sessions_24h: 2, + account_changes_24h: 0, + agent_errors_24h: 0, + top_departments: Vec::new(), + last_event_utc: Some("2026-06-04T10:00:00Z".to_string()), + query_ms: 7, + fallback_used: false, + error: None, + }; + assert!(security_events_summary_text(&available).contains("События безопасности доступны")); + } + #[test] fn reports_include_commercial_kpis_and_disclaimer() { let snapshot = Snapshot { diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index d142cd6..8fcfdab 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -2156,18 +2156,42 @@ function renderSecurityEventsSummary(summary, options = {}) { const disabled = backend === "disabled" || s.status === "disabled" || !summary; const fallback = Boolean(s.fallback_used); const status = disabled ? "UNKNOWN" : fallback ? "WARN" : Number(s.events_24h || 0) > 0 ? "WARN" : "OK"; + const stateText = disabled + ? "Источник событий безопасности отключён" + : fallback + ? "События безопасности временно недоступны" + : "События безопасности доступны"; + const localModeText = disabled ? "Используется локальный режим без ClickHouse" : ""; const title = options.compact ? "События безопасности" : "События безопасности за 24 часа"; const subtitle = disabled - ? "Агрегированный источник событий безопасности отключён." + ? `${stateText}. ${localModeText}.` : fallback - ? "ClickHouse недоступен, портал работает в резервном режиме без событий безопасности." + ? "Портал работает в резервном режиме без событий безопасности." : "Агрегированная сводка без сырых журналов и без автоматического создания инцидентов."; const top = Array.isArray(s.top_departments) ? s.top_departments.slice(0, 5) : []; const warning = fallback ? `
${ui(stateText)}
+