fix(portal): clarify unassigned department labels

This commit is contained in:
igor04091968
2026-06-06 19:10:00 +03:00
parent 838a94f265
commit 53a0e4b2ab
2 changed files with 21 additions and 11 deletions
+11 -3
View File
@@ -31,7 +31,8 @@ const API_CONTRACT_OPENAPI: &str = include_str!("contracts/openapi.json");
const API_CONTRACT_TYPESCRIPT: &str = include_str!("contracts/typescript.d.ts"); const API_CONTRACT_TYPESCRIPT: &str = include_str!("contracts/typescript.d.ts");
const UEBA_BASELINE_MIN_SAMPLES: usize = 3; const UEBA_BASELINE_MIN_SAMPLES: usize = 3;
const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(120); const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(120);
const DEFAULT_DEPARTMENT_LABEL: &str = "Без подразделения"; const DEFAULT_DEPARTMENT_LABEL: &str = "Не привязано к подразделению";
const LEGACY_UNASSIGNED_DEPARTMENT_LABEL: &str = "Без подразделения";
#[cfg(unix)] #[cfg(unix)]
const SIGKILL: i32 = 9; const SIGKILL: i32 = 9;
@@ -2165,7 +2166,10 @@ fn display_name_opt(value: Option<&str>, fallback: &str) -> String {
return fallback.to_string(); return fallback.to_string();
}; };
let value = value.trim(); let value = value.trim();
if value.is_empty() || has_broken_display_chars(value) { if value.is_empty()
|| value == LEGACY_UNASSIGNED_DEPARTMENT_LABEL
|| has_broken_display_chars(value)
{
fallback.to_string() fallback.to_string()
} else { } else {
value.to_string() value.to_string()
@@ -2670,7 +2674,7 @@ FORMAT JSONEachRow
let top_sql = format!( let top_sql = format!(
r#" r#"
WITH now() - INTERVAL 24 HOUR AS since WITH now() - INTERVAL 24 HOUR AS since
SELECT if(empty(infobase), 'Без подразделения', infobase) AS department, toUInt64(count()) AS events SELECT if(empty(infobase), 'Не привязано к подразделению', infobase) AS department, toUInt64(count()) AS events
FROM {database}.entity_timeline FROM {database}.entity_timeline
WHERE ts >= since WHERE ts >= since
GROUP BY department GROUP BY department
@@ -10336,6 +10340,10 @@ mod tests {
fn display_department_name_replaces_corrupt_runtime_labels() { fn display_department_name_replaces_corrupt_runtime_labels() {
assert_eq!(display_department_name(Some("Бухгалтерия")), "Бухгалтерия"); assert_eq!(display_department_name(Some("Бухгалтерия")), "Бухгалтерия");
assert_eq!(display_department_name(Some("")), DEFAULT_DEPARTMENT_LABEL); assert_eq!(display_department_name(Some("")), DEFAULT_DEPARTMENT_LABEL);
assert_eq!(
display_department_name(Some("Без подразделения")),
DEFAULT_DEPARTMENT_LABEL
);
assert_eq!( assert_eq!(
display_department_name(Some("\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}")), display_department_name(Some("\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}")),
DEFAULT_DEPARTMENT_LABEL DEFAULT_DEPARTMENT_LABEL
@@ -17,6 +17,8 @@ const state = {
}, },
}; };
const DEFAULT_DEPARTMENT_LABEL = "Не привязано к подразделению";
const VIEW_MODES = { const VIEW_MODES = {
executive: { executive: {
label: "Руководитель", label: "Руководитель",
@@ -802,7 +804,7 @@ function departmentRows(report) {
const status = item.status || "INFO"; const status = item.status || "INFO";
const responsible = item.responsible || departmentResponsible(owners, index); const responsible = item.responsible || departmentResponsible(owners, index);
return { return {
label: item.label || "Без подразделения", label: item.label || DEFAULT_DEPARTMENT_LABEL,
activity, activity,
activityText: Number.isFinite(activity) ? `${Math.round(activity)}%` : firstPercent(item.value), activityText: Number.isFinite(activity) ? `${Math.round(activity)}%` : firstPercent(item.value),
deviation: item.deviation || departmentDeviation(item), deviation: item.deviation || departmentDeviation(item),
@@ -1392,7 +1394,7 @@ function renderInvestigationPacks(candidates) {
</div> </div>
<div class="list compact-list">${rows.length ? rows.map(item => ` <div class="list compact-list">${rows.length ? rows.map(item => `
<div class="row compact-row"> <div class="row compact-row">
<strong>${ui(item.department || "Без подразделения")}</strong> <strong>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</strong>
<span class="muted">${ui(item.reason || "требуется проверка")} · ${ui(item.hostname || "-")}</span> <span class="muted">${ui(item.reason || "требуется проверка")} · ${ui(item.hostname || "-")}</span>
<a class="small-button" href="${apiBase()}${withRole(`/investigation-pack/${encodeURIComponent(item.id || "")}?format=markdown`, "forensics")}" download>Скачать</a> <a class="small-button" href="${apiBase()}${withRole(`/investigation-pack/${encodeURIComponent(item.id || "")}?format=markdown`, "forensics")}" download>Скачать</a>
</div> </div>
@@ -2364,7 +2366,7 @@ function renderSecurityEventsSummary(summary, options = {}) {
<tbody> <tbody>
${top.length ? top.map(item => ` ${top.length ? top.map(item => `
<tr> <tr>
<td>${ui(item.department || "Без подразделения")}</td> <td>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</td>
<td>${ui(item.events ?? 0)}</td> <td>${ui(item.events ?? 0)}</td>
</tr> </tr>
`).join("") : ` `).join("") : `
@@ -2376,7 +2378,7 @@ function renderSecurityEventsSummary(summary, options = {}) {
</tbody> </tbody>
</table> </table>
</div> </div>
<p class="muted small">Последнее событие: ${ui(s.last_event_utc || "нет данных")} · запрос ${ui(s.query_ms ?? 0)} ms.</p> <p class="muted small">${ui(DEFAULT_DEPARTMENT_LABEL)} — событие есть в источнике, но в нем не передана связь с оргструктурой. Последнее событие: ${ui(s.last_event_utc || "нет данных")} · запрос ${ui(s.query_ms ?? 0)} ms.</p>
`} `}
</section> </section>
`; `;
@@ -2408,7 +2410,7 @@ function renderBusinessRisk(items) {
<tbody> <tbody>
${rows.length ? rows.map(item => ` ${rows.length ? rows.map(item => `
<tr> <tr>
<td><strong>${ui(item.department || "Без подразделения")}</strong></td> <td><strong>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</strong></td>
<td><span class="badge ${statusClass(item.risk_level)}">${ui(item.risk_level || "UNKNOWN")}</span></td> <td><span class="badge ${statusClass(item.risk_level)}">${ui(item.risk_level || "UNKNOWN")}</span></td>
<td>${ui(businessRiskReasons(item))}</td> <td>${ui(businessRiskReasons(item))}</td>
<td>${ui(item.security_events_24h ?? 0)}</td> <td>${ui(item.security_events_24h ?? 0)}</td>
@@ -2459,7 +2461,7 @@ function renderRiskHeatmap(items) {
<tbody> <tbody>
${rows.length ? rows.map(item => ` ${rows.length ? rows.map(item => `
<tr class="heatmap-row heatmap-${escapeHtml(String(item.heat_level || "unknown").toLowerCase())}"> <tr class="heatmap-row heatmap-${escapeHtml(String(item.heat_level || "unknown").toLowerCase())}">
<td><strong>${ui(item.department || "Без подразделения")}</strong></td> <td><strong>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</strong></td>
<td>${ui(riskPercentText(item.trust_kpi_score))}</td> <td>${ui(riskPercentText(item.trust_kpi_score))}</td>
<td>${ui(riskPercentText(item.activity_score))}</td> <td>${ui(riskPercentText(item.activity_score))}</td>
<td>${ui(riskPercentText(item.agent_coverage_pct))}</td> <td>${ui(riskPercentText(item.agent_coverage_pct))}</td>
@@ -2546,7 +2548,7 @@ function renderSecurityCorrelation(items) {
<tbody> <tbody>
${rows.length ? rows.map(item => ` ${rows.length ? rows.map(item => `
<tr> <tr>
<td><strong>${ui(item.department || "Без подразделения")}</strong></td> <td><strong>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</strong></td>
<td>${ui(riskPercentText(item.trust_kpi_score))}</td> <td>${ui(riskPercentText(item.trust_kpi_score))}</td>
<td>${ui(riskPercentText(item.activity_score))}</td> <td>${ui(riskPercentText(item.activity_score))}</td>
<td><span class="badge ${statusClass(item.business_risk_level)}">${ui(item.business_risk_level || "UNKNOWN")}</span></td> <td><span class="badge ${statusClass(item.business_risk_level)}">${ui(item.business_risk_level || "UNKNOWN")}</span></td>
@@ -2625,7 +2627,7 @@ function renderBusinessRiskTimeline(history, summary) {
${rows.length ? rows.map(item => ` ${rows.length ? rows.map(item => `
<tr> <tr>
<td>${ui(item.date || "-")}</td> <td>${ui(item.date || "-")}</td>
<td><strong>${ui(item.department || "Без подразделения")}</strong></td> <td><strong>${ui(item.department || DEFAULT_DEPARTMENT_LABEL)}</strong></td>
<td><span class="badge ${statusClass(item.risk_level)}">${ui(item.risk_level || "UNKNOWN")}</span></td> <td><span class="badge ${statusClass(item.risk_level)}">${ui(item.risk_level || "UNKNOWN")}</span></td>
<td>${ui(Array.isArray(item.reasons) && item.reasons.length ? item.reasons.join("; ") : "нет существенных причин")}</td> <td>${ui(Array.isArray(item.reasons) && item.reasons.length ? item.reasons.join("; ") : "нет существенных причин")}</td>
</tr> </tr>