fix(portal): polish security events fallback states

This commit is contained in:
igor04091968
2026-06-04 22:16:12 +03:00
parent da17047b38
commit 8957161317
5 changed files with 157 additions and 6 deletions
+44 -3
View File
@@ -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 {
@@ -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
? `<div class="quality-warning">События безопасности временно недоступны. Проверьте ClickHouse и переменные SECURITY_EVENTS_BACKEND/CLICKHOUSE_*.</div>`
: "";
if (options.compact) {
return `
<section class="card security-events-card">
<div class="section-head">
<div>
<h3 ${tooltip("Краткая управленческая сводка доступности событий безопасности за последние 24 часа.")}>${ui(title)}</h3>
<p class="muted">${ui(stateText)}</p>
</div>
<span class="badge ${statusClass(status)}">${ui(status)}</span>
</div>
<div class="quality-grid">
<div><span class="muted">Состояние</span><strong>${ui(stateText)}</strong></div>
<div><span class="muted">Событий за 24 часа</span><strong>${ui(s.events_24h ?? 0)}</strong></div>
<div><span class="muted">Режим</span><strong>${ui(disabled ? localModeText : "Агрегированная сводка")}</strong></div>
</div>
</section>
`;
}
return `
<section class="card security-events-card">
<div class="section-head">
@@ -2178,6 +2202,7 @@ function renderSecurityEventsSummary(summary, options = {}) {
<span class="badge ${statusClass(status)}">${ui(status)}</span>
</div>
<div class="quality-grid">
<div><span class="muted">Состояние</span><strong>${ui(stateText)}</strong></div>
<div><span class="muted">Источник</span><strong>${ui(backend)}</strong></div>
<div><span class="muted">Событий</span><strong>${ui(s.events_24h ?? 0)}</strong></div>
<div><span class="muted">Неуспешные входы</span><strong>${ui(s.failed_logins_24h ?? 0)}</strong></div>