feat(detmir): focus security incidents portal card
This commit is contained in:
@@ -1603,6 +1603,22 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
|
||||
failed units stayed `0`. Local Playwright smoke against a temporary
|
||||
portal instance verified `Инциденты ИБ`, `В работу`, persisted UI state,
|
||||
disabled ack button, and zero JS errors.
|
||||
- `Инциденты ИБ` tightened to a DLP-focused card: the tab now filters the
|
||||
card to DLP/incident/case items, adds a synthetic DLP-count incident when
|
||||
`dlp_counts.warn/fail` is non-zero, and shows only DLP/Grafana dashboard
|
||||
links (`detmir-dlp-security`, `detmir-dlp-management`,
|
||||
`awatch-dlp-overview`, Grafana catalog). Worktime, 1C, AW UI, and other
|
||||
non-incident operational links are intentionally not shown in this card.
|
||||
Local Playwright smoke verified DLP incident rendering, dashboard links,
|
||||
no Worktime/1C text in the card, and zero JS errors.
|
||||
- during this deploy, `detmir-grafana-check` was corrected so empty
|
||||
detail-only panels for employees/applications are WARN, not FAIL. The
|
||||
mandatory freshness/summary panels still fail the check when stale or
|
||||
empty. This removed a false red state shortly after midnight when
|
||||
`today` detail rows were legitimately empty. Production verification:
|
||||
Grafana check `ok=true` with `fail=0`, `detmir-auto --no-heal` rc `0`,
|
||||
portal health `true`, `detmir-status` `OK / ok_for_operator=true`, and
|
||||
failed units `0`.
|
||||
|
||||
Отложить:
|
||||
|
||||
|
||||
@@ -232,8 +232,14 @@ fn build_report(cli: &Cli) -> Result<Report> {
|
||||
&& first_number
|
||||
.map(|value| value > cli.max_freshness_minutes)
|
||||
.unwrap_or(true);
|
||||
let status = if rows < cli.min_panel_rows || freshness_failed {
|
||||
let empty_optional_panel =
|
||||
rows < cli.min_panel_rows && is_optional_empty_panel(&target.panel_title);
|
||||
let status = if freshness_failed
|
||||
|| (rows < cli.min_panel_rows && !empty_optional_panel)
|
||||
{
|
||||
Status::Fail
|
||||
} else if empty_optional_panel {
|
||||
Status::Warn
|
||||
} else {
|
||||
Status::Ok
|
||||
};
|
||||
@@ -572,6 +578,11 @@ fn is_freshness_panel(title: &str) -> bool {
|
||||
title.to_lowercase().contains("свеж")
|
||||
}
|
||||
|
||||
fn is_optional_empty_panel(title: &str) -> bool {
|
||||
let title = title.to_lowercase();
|
||||
title.contains("прилож") || title.contains("сотрудник")
|
||||
}
|
||||
|
||||
fn frame_row_count(value: &Value) -> usize {
|
||||
value
|
||||
.get("frames")
|
||||
@@ -708,4 +719,18 @@ mod tests {
|
||||
assert_eq!(targets[0].panel_id, 5);
|
||||
assert_eq!(targets[0].datasource_uid, "influxdb_aw");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_panels_are_optional_when_empty() {
|
||||
assert!(is_optional_empty_panel(
|
||||
"Сегодня: доказанная работа по приложениям"
|
||||
));
|
||||
assert!(is_optional_empty_panel(
|
||||
"Сегодня: приложения и подтверждения"
|
||||
));
|
||||
assert!(is_optional_empty_panel(
|
||||
"Сегодня: активность по сотрудникам"
|
||||
));
|
||||
assert!(!is_optional_empty_panel("Сегодня: активное время"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,9 @@ struct PortalLinks {
|
||||
portal: String,
|
||||
grafana_dashboards: String,
|
||||
detmir_activitywatch: String,
|
||||
dlp_security_dashboard: String,
|
||||
dlp_management_dashboard: String,
|
||||
dlp_overview_dashboard: String,
|
||||
aw_ui: String,
|
||||
worktime_report: String,
|
||||
file1c_brief: String,
|
||||
@@ -689,6 +692,22 @@ fn build_incidents(snapshot: &Snapshot, state: &IncidentStateFile) -> Vec<Incide
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(status) = snapshot.detmir_status.payload.as_ref() {
|
||||
let counts = status.get("dlp_counts").unwrap_or(&Value::Null);
|
||||
let warn = counts.get("warn").and_then(Value::as_u64).unwrap_or(0);
|
||||
let fail = counts.get("fail").and_then(Value::as_u64).unwrap_or(0);
|
||||
if warn > 0 || fail > 0 {
|
||||
incidents.push(incident_item(
|
||||
if fail > 0 { "FAIL" } else { "WARN" },
|
||||
"dlp",
|
||||
"dlp_counts",
|
||||
&format!("DLP requires review: warn={warn}, fail={fail}"),
|
||||
&snapshot.generated_at_utc,
|
||||
"/portal/incidents",
|
||||
state,
|
||||
));
|
||||
}
|
||||
}
|
||||
incidents
|
||||
}
|
||||
|
||||
@@ -923,6 +942,12 @@ fn links() -> PortalLinks {
|
||||
detmir_activitywatch:
|
||||
"/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m"
|
||||
.to_string(),
|
||||
dlp_security_dashboard:
|
||||
"/d/detmir-dlp-security?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
|
||||
dlp_management_dashboard:
|
||||
"/d/detmir-dlp-management?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
|
||||
dlp_overview_dashboard:
|
||||
"/d/awatch-dlp-overview?orgId=1&from=now-30d&to=now&timezone=browser".to_string(),
|
||||
aw_ui: "/r/aw/".to_string(),
|
||||
worktime_report: "/reports/worktime/management?format=html&host=SHARKON2025".to_string(),
|
||||
file1c_brief: "/r/file1c/brief".to_string(),
|
||||
|
||||
@@ -73,6 +73,16 @@ function renderLinks(links) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderDlpLinks(links) {
|
||||
const link = (text, href) => `<a class="button" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
|
||||
return `<div class="links">
|
||||
${link("ИБ дашборд", links.dlp_security_dashboard)}
|
||||
${link("ИБ для руководства", links.dlp_management_dashboard)}
|
||||
${link("DLP обзор", links.dlp_overview_dashboard)}
|
||||
${link("Все Grafana dashboards", links.grafana_dashboards)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderSourceList(data) {
|
||||
const sources = [
|
||||
["DetMir", data.detmir_status],
|
||||
@@ -181,14 +191,36 @@ function renderIncidentsList(items) {
|
||||
`).join("")}</div>`;
|
||||
}
|
||||
|
||||
function isDlpIncident(item) {
|
||||
const text = `${item?.kind || ""} ${item?.source || ""} ${item?.summary || ""}`.toLowerCase();
|
||||
return text.includes("dlp") || text.includes("incident") || text.includes("case") || text.includes("иб");
|
||||
}
|
||||
|
||||
function renderDlpIncidentsList(items) {
|
||||
const dlpItems = (items || []).filter(isDlpIncident);
|
||||
if (dlpItems.length === 0) return `<p class="muted">Активных DLP/ИБ-инцидентов нет.</p>`;
|
||||
return renderIncidentsList(dlpItems);
|
||||
}
|
||||
|
||||
function renderIncidents(data) {
|
||||
const links = state.links || {};
|
||||
return `
|
||||
<h2 class="section-title">Инциденты ИБ</h2>
|
||||
${renderIncidentsList(data)}
|
||||
<div class="grid-2">
|
||||
<section class="card">
|
||||
<h3>DLP-инциденты</h3>
|
||||
${renderDlpIncidentsList(data)}
|
||||
</section>
|
||||
<section class="card">
|
||||
<h3>Графики и дашборды</h3>
|
||||
${renderDlpLinks(links)}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!state.links) state.links = await loadJson("/links");
|
||||
const summary = await loadJson("/summary");
|
||||
renderSummary(summary);
|
||||
const content = document.getElementById("content");
|
||||
|
||||
Reference in New Issue
Block a user