From 4197efe48286844a302ad06a95ec395e9c294114 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Thu, 4 Jun 2026 07:09:30 +0300 Subject: [PATCH] fix(portal): use management language in navigation --- .../crates/detmir-portal/src/static/app.js | 248 +++++++++++------- .../detmir-portal/src/static/index.html | 12 +- docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md | 30 +-- 3 files changed, 176 insertions(+), 114 deletions(-) diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index 847c350..cdce4ae 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -37,17 +37,79 @@ function escapeHtml(value) { .replaceAll('"', """); } +function displayText(value) { + return String(value ?? "") + .replaceAll("Workforce", "Работа сотрудников") + .replaceAll("workforce", "работа сотрудников") + .replaceAll("Security", "Безопасность") + .replaceAll("Forensics", "Расследования") + .replaceAll("UEBA риск", "Индекс риска") + .replaceAll("UEBA", "оценка риска") + .replaceAll("DLP/ИБ", "ИБ") + .replaceAll("DLP", "Проверки ИБ") + .replaceAll("Evidence metadata", "Материалы проверки") + .replaceAll("Evidence", "Материалы") + .replaceAll("evidence", "материалы") + .replaceAll("Grafana dashboards", "графики") + .replaceAll("Grafana data", "данные графиков") + .replaceAll("Grafana", "Графики") + .replaceAll("DetMir ActivityWatch", "Журнал активности") + .replaceAll("AW UI", "Журнал активности") + .replaceAll("bundle", "пакет проверки") + .replaceAll("Checksum", "Контрольная сумма") + .replaceAll("Markdown", "Текстовый отчет") + .replaceAll("JSON", "Данные") + .replaceAll("proxy:", "расчет:") + .replaceAll("rule-based", "по правилам") + .replaceAll("Read-only risk score", "Оценка риска без автоматического воздействия") + .replaceAll("Confidence", "Достоверность") + .replaceAll("sources", "источники") + .replaceAll("baseline", "обычный профиль") + .replaceAll("policy", "правила") + .replaceAll("default_weight", "нет явного правила") + .replaceAll("App time", "Время приложений") + .replaceAll("Weighted", "С учетом правил") + .replaceAll("Worktime", "Рабочее время") + .replaceAll("Management report", "Сводка руководителя") + .replaceAll("Drill-down", "Разбор") + .replaceAll("KPI", "показатель") + .replaceAll("items", "записи") + .replaceAll("medium risk", "средний риск") + .replaceAll("low risk", "низкий риск") + .replaceAll("high risk", "высокий риск") + .replaceAll("critical risk", "критический риск") + .replaceAll("weighted_seconds", "время с учетом правил") + .replaceAll("planned_seconds", "плановое время") + .replaceAll("active_seconds", "активное время") + .replaceAll("app-weight", "вес приложений") + .replaceAll("per-user", "по сотруднику") + .replaceAll("user:", "сотрудник:") + .replaceAll("dept:", "подразделение:") + .replaceAll("yes", "да") + .replaceAll("no", "нет") + .replaceAll("daily", "день") + .replaceAll("weekly", "неделя") + .replaceAll("monthly", "месяц") + .replaceAll("OK", "OK") + .replaceAll("FAIL", "FAIL") + .replaceAll("WARN", "WARN"); +} + +function ui(value) { + return escapeHtml(displayText(value)); +} + function renderSummary(summary, readiness) { const global = document.getElementById("globalStatus"); global.className = `status-pill ${statusClass(summary.severity)}`; - global.textContent = `Сенсоры ${summary.operator_ok ? "OK" : "NO"} · ${summary.severity}`; + global.textContent = `Сбор данных ${summary.operator_ok ? "OK" : "NO"} · ${summary.severity}`; const blocks = Object.entries(summary.blocks || {}); const readinessCard = renderReadinessSummaryCard(readiness); document.getElementById("summary").innerHTML = readinessCard + blocks.map(([name, block]) => `
${escapeHtml(block.status)} -

${escapeHtml(label(name))}

-

${escapeHtml(block.text)}

+

${ui(label(name))}

+

${ui(block.text)}

`).join(""); } @@ -71,14 +133,14 @@ function renderReadinessSummaryCard(readiness) { ${escapeHtml(status)}

Готовность системы

- +
Дата${escapeHtml(generated)}
Подпись${signatureOk ? "OK" : "FAIL"}
-
Checksum${checksumOk ? "OK" : "FAIL"}
+
Контрольная сумма${checksumOk ? "OK" : "FAIL"}
-

Отпечаток: ${escapeHtml(shortFingerprint(fingerprint))}

+

Отпечаток ключа: ${escapeHtml(shortFingerprint(fingerprint))}

${escapeHtml(verificationText)}

`; @@ -93,12 +155,12 @@ function shortFingerprint(value) { function label(name) { return { collection: "Сбор данных", - grafana: "Grafana", - dlp: "DLP", + grafana: "Графики", + dlp: "Проверки ИБ", worktime: "Работа сегодня", one_c: "1С", - work: "Workforce", - security: "Security" + work: "Работа сотрудников", + security: "Безопасность" }[name] || name; } @@ -116,9 +178,9 @@ function metricCard(labelText, value, status, context) { return `
${escapeHtml(status || "INFO")} -

${escapeHtml(labelText)}

-

${escapeHtml(value ?? "Нет данных")}

-

${escapeHtml(context || "")}

+

${ui(labelText)}

+

${ui(value ?? "Нет данных")}

+

${ui(context || "")}

`; } @@ -133,11 +195,11 @@ function renderExecutiveMetrics(report, incidents) { const openCount = Array.isArray(incidents) ? incidents.filter(item => !item.acknowledged).length : 0; return `
- ${metricCard("Индекс активности за день", activity?.value, activity?.status, activity?.context || "proxy: активное время / плановое время")} + ${metricCard("Индекс активности за день", activity?.value, activity?.status, activity?.context || "расчет: активное время / плановое время")} ${metricCard("Активные сотрудники", employees?.value, employees?.status, employees?.context)} ${metricCard("Отклонения от нормы", open?.value ?? openCount, open?.status || incidentStatusFromCount(openCount), "вопросы, требующие реакции")} ${metricCard("Подразделения с просадкой", departments?.value, departments?.status, departments?.context || "сравнение текущего дня")} - ${metricCard("Риски ИБ", risk?.value, risk?.status, risk?.context || "rule-based UEBA v1")} + ${metricCard("Риски ИБ", risk?.value, risk?.status, risk?.context || "оценка по правилам")} ${metricCard("Новые инциденты", open?.value ?? openCount, open?.status || incidentStatusFromCount(openCount), "не взятые в работу")} ${metricCard("Готовность отчета", reportReadinessText(report), report?.operator_ok ? "OK" : report?.severity, "daily / weekly / monthly")} ${metricCard("Доказательная база", evidence?.value, evidence?.status, evidence?.context)} @@ -154,9 +216,9 @@ function incidentStatusFromCount(count) { function reportReadinessText(report) { const trend = report?.workforce?.trend_status || "daily_only"; - if (trend === "monthly_ready") return "daily / weekly / monthly"; - if (trend === "weekly_ready") return "daily / weekly"; - return "daily"; + if (trend === "monthly_ready") return "день / неделя / месяц"; + if (trend === "weekly_ready") return "день / неделя"; + return "день"; } function renderSectionItems(section, emptyText) { @@ -166,8 +228,8 @@ function renderSectionItems(section, emptyText) { } return `
${items.slice(0, 12).map(item => `
- ${escapeHtml(item.label || "-")} - ${escapeHtml(item.value || "")} + ${ui(item.label || "-")} + ${ui(item.value || "")} ${escapeHtml(item.status || "INFO")}
`).join("")}
`; @@ -176,18 +238,18 @@ function renderSectionItems(section, emptyText) { function renderReportTypeCards() { const types = [ ["Ежедневный отчет", "Оперативный срез за сегодня", "OK"], - ["Недельный отчет", "Динамика после накопления daily history", "INFO"], + ["Недельный отчет", "Динамика после накопления истории по дням", "INFO"], ["Месячный отчет", "Готов для управленческой аналитики после накопления истории", "INFO"], ["По подразделению", "Сравнение загрузки и просадок", "OK"], - ["По сотруднику", "Drill-down с осторожной трактовкой KPI", "WARN"], + ["По сотруднику", "Разбор показателя с осторожной трактовкой", "WARN"], ["По инциденту", "Материалы для служебной проверки", "OK"], ["Акт пилота", "Итоги опытной эксплуатации", "OK"] ]; return `
${types.map(([title, text, status]) => `
${escapeHtml(status)} -

${escapeHtml(title)}

-

${escapeHtml(text)}

+

${ui(title)}

+

${ui(text)}

`).join("")}
`; } @@ -195,9 +257,9 @@ function renderReportTypeCards() { function renderLinks(links) { const link = (text, href) => `${escapeHtml(text)}`; return `
- + - JSON + Скачать данные

Ключевые показатели

${renderKpiCards(data.kpis)} @@ -631,8 +693,8 @@ function renderReports(data) {

Срезы отчета

${renderReportSections(data.sections)}
-

Markdown для отчета

-
${escapeHtml(data.markdown || "")}
+

Текст для отчета

+
${ui(data.markdown || "")}
`; } @@ -730,10 +792,10 @@ async function verifyReadinessBundle(button) { }; const status = document.getElementById("readinessVerifyStatus"); if (status) { - status.textContent = `Проверено: checksum ${verify.checksum_verified ? "OK" : "FAIL"} · signature ${verify.signature_verified ? "OK" : "FAIL"}`; + status.textContent = `Проверено: контрольная сумма ${verify.checksum_verified ? "OK" : "FAIL"} · подпись ${verify.signature_verified ? "OK" : "FAIL"}`; } button.disabled = false; - button.textContent = "Проверить bundle"; + button.textContent = "Проверить пакет"; } async function anonymizeReport(button) { @@ -744,7 +806,7 @@ async function anonymizeReport(button) { async function exportMarkdown() { const data = state.reports || await loadJson("/reports"); - const markdown = data.markdown || ""; + const markdown = displayText(data.markdown || ""); const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); diff --git a/adk-rust/crates/detmir-portal/src/static/index.html b/adk-rust/crates/detmir-portal/src/static/index.html index e640994..ff4d15c 100644 --- a/adk-rust/crates/detmir-portal/src/static/index.html +++ b/adk-rust/crates/detmir-portal/src/static/index.html @@ -11,7 +11,7 @@

AWatch-rus

-

Workforce / Security / Forensics

+

Активность / Риски / Расследования

Рабочий кабинет руководителя, ИБ и расследователя

@@ -38,8 +38,8 @@
- - + +
Загрузка
@@ -47,9 +47,9 @@ diff --git a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md index cb37158..138ef04 100644 --- a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md +++ b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md @@ -6,19 +6,19 @@ Портал переведен из технической "админки логов" в рабочий кабинет для трех ролей: -- руководитель: управленческая сводка, Workforce KPI, отклонения, отчеты; -- ИБ: риск-срез, DLP/evidence, состояние контрольных сигналов; -- расследователь: карточки инцидентов, evidence, хеши и выгрузка материалов. +- руководитель: управленческая сводка, показатели активности, отклонения, отчеты; +- ИБ: риск-срез, проверки данных, состояние контрольных сигналов; +- расследователь: карточки инцидентов, материалы проверки, хеши и выгрузка материалов. ## Что изменено -- Верхняя панель переименована в `AWatch-rus Workforce / Security / Forensics`. -- Добавлены элементы отчета: период, подразделение, ответственный, экспорт Markdown/PDF, статус сенсоров. +- Верхняя панель переименована в `AWatch-rus Активность / Риски / Расследования`. +- Добавлены элементы отчета: период, подразделение, ответственный, текстовый отчет/PDF, статус сбора данных. - Навигация приведена к продуктовой модели: - `Сводка`; - - `Workforce`; - - `Security`; - - `Forensics`; + - `Работа сотрудников`; + - `Риски и безопасность`; + - `Расследования`; - `Отчеты`. - Первый экран показывает executive summary: - индекс активности за день; @@ -29,16 +29,16 @@ - новые инциденты; - готовность отчета; - доказательная база. -- Workforce-блок показывает загрузку, простои, перегруз, приложения, объяснение индекса и policy audit. -- Security-блок использует темную тему и выводит риск/ИБ-срез без автоматического воздействия на сеть. -- Forensics-блок показывает инциденты, evidence, скриншоты, хеши и ссылки на выгрузку. +- Блок работы сотрудников показывает загрузку, простои, перегруз, приложения, объяснение индекса и проверку правил. +- Блок рисков использует темную тему и выводит риск/ИБ-срез без автоматического воздействия на сеть. +- Блок расследований показывает инциденты, материалы, скриншоты, хеши и ссылки на выгрузку. - Отчеты дополнены типами: ежедневный, недельный, месячный, по подразделению, по сотруднику, по инциденту, акт пилота. ## Важные ограничения - Фильтры периода, подразделения и ответственного пока являются UI-контролами. Backend-фильтрация должна добавляться отдельным изменением, чтобы не искажать текущие данные. -- `Индекс активности` остается proxy-метрикой: активное время / плановое время. Weighted KPI объясняется отдельно через role/application policy. -- Security/Forensics показывают риск-срез и evidence. Это не заявляется как сертифицированная СЗИ. +- `Индекс активности` остается расчетной метрикой: активное время / плановое время. Взвешенный показатель объясняется отдельно через правила ролей и приложений. +- Разделы рисков и расследований показывают риск-срез и материалы проверки. Это не заявляется как сертифицированная СЗИ. - pfSense и сетевой периметр этим изменением не затрагивались. ## Проверки @@ -73,7 +73,7 @@ cargo build --release --manifest-path adk-rust/Cargo.toml -p detmir-portal Playwright smoke: - заголовок страницы: `AWatch-rus Portal`; -- вкладки `Сводка`, `Workforce`, `Security`, `Forensics`, `Отчеты` открываются; +- вкладки `Сводка`, `Работа сотрудников`, `Риски и безопасность`, `Расследования`, `Отчеты` открываются; - JS-ошибок нет; - API-ответов с кодом 400+ нет; -- `Security` включает темную тему. +- раздел рисков включает темную тему.