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)}
Готовность системы
- Проверить bundle
+ Проверить пакет
Дата ${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 `
- ${link("DetMir ActivityWatch", links.detmir_activitywatch)}
- ${link("Grafana", links.grafana_dashboards)}
- ${link("AW UI", links.aw_ui)}
+ ${link("Журнал активности", links.detmir_activitywatch)}
+ ${link("Графики", links.grafana_dashboards)}
+ ${link("События рабочих мест", links.aw_ui)}
${link("Рабочее время", links.worktime_report)}
${link("1С сводка", links.file1c_brief)}
${link("1С действия", links.file1c_actions)}
@@ -207,10 +269,10 @@ function renderLinks(links) {
function renderDlpLinks(links) {
const link = (text, href) => `
${escapeHtml(text)} `;
return `
- ${link("ИБ дашборд", links.dlp_security_dashboard)}
+ ${link("ИБ-графики", links.dlp_security_dashboard)}
${link("ИБ для руководства", links.dlp_management_dashboard)}
- ${link("DLP обзор", links.dlp_overview_dashboard)}
- ${link("Все Grafana dashboards", links.grafana_dashboards)}
+ ${link("Обзор рисков данных", links.dlp_overview_dashboard)}
+ ${link("Все графики", links.grafana_dashboards)}
`;
}
@@ -218,13 +280,13 @@ function renderSourceList(data) {
const sources = [
["DetMir", data.detmir_status],
["Проверки", data.detmir_check],
- ["Systemd", data.failed_units],
- ["Grafana data", data.grafana_data]
+ ["Сервисы", data.failed_units],
+ ["Данные графиков", data.grafana_data]
];
return `
${sources.map(([name, source]) => `
- ${escapeHtml(name)}
- ${escapeHtml(source?.summary || source?.error || "нет данных")}
+ ${ui(name)}
+ ${ui(source?.summary || source?.error || "нет данных")}
${escapeHtml(source?.status || (source?.ok ? "OK" : "FAIL"))}
`).join("")}
`;
@@ -247,8 +309,8 @@ function renderOperator(data, report) {
${renderExecutiveMetrics(report, incidents)}
Рабочая активность сотрудников загрузка, простои, перегруз и дисциплина процессов
- ${renderSectionItems(workforce, "Workforce-срез пока не сформирован.")}
- ${renderSectionItems(insights, "Отклонений Workforce пока не найдено.")}
+ ${renderSectionItems(workforce, "Срез по работе сотрудников пока не сформирован.")}
+ ${renderSectionItems(insights, "Отклонений по работе сотрудников пока не найдено.")}
Контроль безопасности подозрительные события и приоритет реакции
@@ -280,7 +342,7 @@ function renderManager(data, policyExplain) {
Индекс активности
${escapeHtml(workforceIndex)}
- proxy: активное время / плановое рабочее время
+ расчет: активное время / плановое рабочее время
Сотрудников: ${data.users_count}; активных часов: ${Number(data.total_active_hours || 0).toFixed(1)}
${escapeHtml(data.status?.text || "")}
@@ -334,7 +396,7 @@ function renderWorkforceIndexExplanation(policy) {
return `
Почему такой индекс?
- Role/application policy не настроена. Доступен только нейтральный индекс активности.
+ Правила ролей и приложений не настроены. Доступен только нейтральный индекс активности.
`;
}
@@ -342,13 +404,13 @@ function renderWorkforceIndexExplanation(policy) {
const employees = Array.isArray(policy.employee_details) ? policy.employee_details.slice(0, 12) : [];
const weightedTotal = Math.max(1, Number(policy.weighted_seconds || 0));
const appRows = details.length === 0
- ? `Нет приложений Нет top breakdown для weighted KPI
`
+ ? `Нет приложений Нет разбора по приложениям для взвешенного показателя
`
: details.map(item => {
const contribution = Math.round(Number(item.weighted_seconds || 0) / weightedTotal * 100);
return `
${escapeHtml(item.application || "-")}
- ${escapeHtml(humanSeconds(item.seconds))} · вес ${escapeHtml(pctText(item.weight))} · правило ${escapeHtml(item.matched_rule || "default_weight")}
+ ${escapeHtml(humanSeconds(item.seconds))} · вес ${escapeHtml(pctText(item.weight))} · правило ${ui(item.matched_rule || "нет явного правила")}
${escapeHtml(humanSeconds(item.weighted_seconds))} · ${contribution}%
`;
@@ -358,16 +420,16 @@ function renderWorkforceIndexExplanation(policy) {
Почему такой индекс?
-
${escapeHtml(policy.explanation || "Индекс = взвешенное время приложений / плановое время роли.")}
-
Формула: ${escapeHtml(policy.formula || "index = weighted_seconds / planned_seconds × 100")}.
+
${ui(policy.explanation || "Индекс = взвешенное время приложений / плановое время роли.")}
+
Формула: индекс = время с учетом правил / плановое время × 100.
${escapeHtml(workforceIndexTextFromValue(policy.index))}
Роль ${escapeHtml(policy.role_label || policy.role || "-")}
План ${escapeHtml(humanSeconds(policy.planned_seconds))}
-
App time ${escapeHtml(humanSeconds(policy.app_seconds))}
-
Weighted ${escapeHtml(humanSeconds(policy.weighted_seconds))}
+
Время приложений ${escapeHtml(humanSeconds(policy.app_seconds))}
+
С учетом правил ${escapeHtml(humanSeconds(policy.weighted_seconds))}
${appRows}
${renderPolicyAudit(policy.policy_audit)}
@@ -379,17 +441,17 @@ function renderWorkforceIndexExplanation(policy) {
function renderPolicyAudit(audit) {
const items = Array.isArray(audit?.needs_review) ? audit.needs_review.slice(0, 12) : [];
if (items.length === 0) {
- return `Аудит policy Все top приложения попали под явные правила или данных для аудита нет.
`;
+ return `Проверка правил Ключевые приложения попали под явные правила или данных для проверки нет.
`;
}
return `
-
Аудит policy: default_weight
+
Проверка правил: приложения без явного правила
Эти приложения не нашли явного правила и требуют проверки классификации.
${items.map(item => `
${escapeHtml(item.application || "-")}
- ${escapeHtml(humanSeconds(item.seconds))} · default ${escapeHtml(pctText(item.weight))}
- review
+ ${escapeHtml(humanSeconds(item.seconds))} · вес по умолчанию ${escapeHtml(pctText(item.weight))}
+ проверить
`).join("")}
@@ -400,12 +462,12 @@ function renderEmployeeIndexDetails(items) {
if (!items.length) return "";
return `
-
Drill-down по сотрудникам
-
Это не персональный weighted KPI: per-user индекс сейчас считается по активному времени; app-weight breakdown доступен только на уровне портфеля.
+
Разбор по сотрудникам
+
Это не персональный взвешенный показатель: индекс сотрудника сейчас считается по активному времени; разбор по весам приложений доступен только на уровне общего среза.
${items.map(item => `
${escapeHtml(item.user || "-")}
- ${escapeHtml(item.reason || `${item.formula || "employee_index = active_seconds / planned_seconds × 100"} · active ${humanSeconds(item.active_seconds)} / plan ${humanSeconds(item.planned_seconds)}`)}
+ ${ui(item.reason || `индекс сотрудника = активное время / плановое время × 100 · активно ${humanSeconds(item.active_seconds)} / план ${humanSeconds(item.planned_seconds)}`)}
${escapeHtml(workforceIndexTextFromValue(item.index))}
`).join("")}
@@ -432,23 +494,23 @@ function renderOwner(data) {
Контроль безопасности
-
Приоритеты ИБ: подозрительные подключения, DLP-сигналы, RDP-аномалии и состояние периметра.
+
Приоритеты ИБ: подозрительные подключения, сигналы по данным, RDP-аномалии и состояние периметра.
${escapeHtml(data.summary?.severity || "INFO")}
${cards.map(([name, block]) => `
${escapeHtml(block.status)}
- ${escapeHtml(label(name))}
- ${escapeHtml(block.text)}
+ ${ui(label(name))}
+ ${ui(block.text)}
`).join("")}
Риски с приоритетом
- ${(data.recommendations || []).map(item => `
Рекомендация ${escapeHtml(item)}
`).join("")}
+ ${(data.recommendations || []).map(item => `
Рекомендация ${ui(item)}
`).join("")}
- Дашборды безопасности
+ Графики безопасности
${renderLinks(data.links)}
`;
@@ -459,12 +521,12 @@ function renderIncidentsList(items) {
return `
${items.map(item => `
-
${escapeHtml(item.source)}
-
${escapeHtml(item.kind)} · ${escapeHtml(item.id)}
+
${ui(item.source)}
+
${ui(item.kind)} · ${escapeHtml(item.id)}
-
${escapeHtml(item.summary)}
- ${item.acknowledged ? `
В работе: ${escapeHtml(item.assigned_to || item.actor || "оператор")} · ${escapeHtml(item.comment || "")}
` : ""}
+
${ui(item.summary)}
+ ${item.acknowledged ? `
В работе: ${ui(item.assigned_to || item.actor || "оператор")} · ${ui(item.comment || "")}
` : ""}
${escapeHtml(item.status)}
@@ -482,15 +544,15 @@ function isDlpIncident(item) {
function renderDlpIncidentsList(items) {
const dlpItems = (items || []).filter(isDlpIncident);
- if (dlpItems.length === 0) return `
Активных DLP/ИБ-инцидентов нет.
`;
+ if (dlpItems.length === 0) return `
Активных ИБ-инцидентов по данным нет.
`;
return renderIncidentsList(dlpItems);
}
function renderDlpEvidence(evidence) {
- if (!evidence) return `
Данные evidence загружаются.
`;
- if (!evidence.ok) return `
Evidence недоступны: ${escapeHtml(evidence.error || "ошибка чтения")}
`;
+ if (!evidence) return `
Материалы проверки загружаются.
`;
+ if (!evidence.ok) return `
Материалы недоступны: ${ui(evidence.error || "ошибка чтения")}
`;
const items = evidence.items || [];
- if (items.length === 0) return `
DLP evidence пока не найдены.
`;
+ if (items.length === 0) return `
Материалы по ИБ-инцидентам пока не найдены.
`;
return `
${items.map(item => `
@@ -499,7 +561,7 @@ function renderDlpEvidence(evidence) {
${escapeHtml(item.message || item.file_path || item.rule_id || item.event_id)}
-
${item.source_file ? "Файл: " + escapeHtml(item.source_file) + " · " : ""}${item.screenshot_sha256 ? "SHA-256: " + escapeHtml(item.screenshot_sha256) : escapeHtml(item.blocked_reason || "без скрина")}
+
${item.source_file ? "Файл: " + escapeHtml(item.source_file) + " · " : ""}${item.screenshot_sha256 ? "Контрольный хеш: " + escapeHtml(item.screenshot_sha256) : ui(item.blocked_reason || "без скрина")}
${item.screenshot_available ? "СКРИН" : "МЕТА"}
@@ -518,9 +580,9 @@ function renderIncidents(data) {
Расследования и доказательная база
-
Кто, когда, откуда, куда, связанные хосты, evidence и выгрузка материалов.
+
Кто, когда, откуда, куда, связанные хосты, материалы и выгрузка для проверки.
-
${escapeHtml((incidents || []).length)} items
+
${escapeHtml((incidents || []).length)} записей
@@ -528,12 +590,12 @@ function renderIncidents(data) {
${renderDlpIncidentsList(incidents)}
- Связанные графики и дашборды
+ Связанные графики
${renderDlpLinks(links)}
- Evidence: скриншоты, хеши, файлы
+ Материалы: скриншоты, хеши, файлы
${renderDlpEvidence(evidence)}
`;
@@ -543,9 +605,9 @@ function renderKpiCards(items) {
return `
${(items || []).map(item => `
${escapeHtml(item.status || "INFO")}
- ${escapeHtml(item.label)}
- ${escapeHtml(item.value)}
- ${escapeHtml(item.context || "")}
+ ${ui(item.label)}
+ ${ui(item.value)}
+ ${ui(item.context || "")}
`).join("")}
`;
}
@@ -553,11 +615,11 @@ function renderKpiCards(items) {
function renderReportSections(sections) {
return `
${(sections || []).map(section => `
- ${escapeHtml(section.title)}
+ ${ui(section.title)}
${(section.items || []).map(item => `
- ${escapeHtml(item.label)}
- ${escapeHtml(item.value)}
+ ${ui(item.label)}
+ ${ui(item.value)}
${escapeHtml(item.status || "INFO")}
`).join("")}
@@ -575,21 +637,21 @@ function renderUebaRisk(risk) {
-
UEBA риск
-
${escapeHtml(risk.note || "Read-only risk score без автоматического воздействия.")}
-
Формула: ${escapeHtml(risk.formula || "sum(reason_points) capped at 100")}.
-
Confidence: ${escapeHtml(confidence)} · sources: ${escapeHtml(sources)} · baseline: ${escapeHtml(risk.baseline_status || "-")} · policy: ${escapeHtml(risk.policy_version || "-")}
-
Baseline window: ${escapeHtml(risk.baseline_window_days || "-")} days · available: ${escapeHtml(baselineReady)} · deviation: ${escapeHtml(risk.deviation_score ?? 0)}
+
Индекс риска
+
${ui(risk.note || "Оценка риска без автоматического воздействия.")}
+
Формула: сумма факторов риска, максимум 100.
+
Достоверность: ${escapeHtml(confidence)} · источники: ${ui(sources)} · обычный профиль: ${ui(risk.baseline_status || "-")} · версия правил: ${ui(risk.policy_version || "-")}
+
Окно сравнения: ${escapeHtml(risk.baseline_window_days || "-")} дн. · доступно: ${ui(baselineReady)} · отклонение: ${escapeHtml(risk.deviation_score ?? 0)}
${escapeHtml(risk.level || "unknown")} · ${escapeHtml(risk.score ?? 0)}/100
${reasons.length ? reasons.map(item => `
- ${escapeHtml(item.label || item.code || "-")}
- ${escapeHtml(item.value || "")} · ${escapeHtml(item.recommendation || "")}
+ ${ui(item.label || item.code || "-")}
+ ${ui(item.value || "")} · ${ui(item.recommendation || "")}
+${escapeHtml(item.points || 0)}
- `).join("") : `
Сигналы Существенных UEBA-сигналов в текущем срезе нет. OK
`}
+ `).join("") : `Сигналы Существенных риск-сигналов в текущем срезе нет. OK
`}
`;
}
@@ -608,21 +670,21 @@ function renderReports(data) {
${escapeHtml(data.severity)}
${data.anonymized ? `обезличено ` : ""}
- ${escapeHtml(data.headline)}
- ${escapeHtml(data.period || "")} · обновлено ${escapeHtml(data.generated_at_utc || "")}
+ ${ui(data.headline)}
+ ${ui(data.period || "")} · обновлено ${escapeHtml(data.generated_at_utc || "")}
Для руководителя
${(data.executive_points || []).map(point => `
-
Итог ${escapeHtml(point)}
+
Итог ${ui(point)}
`).join("")}
Ключевые показатели
${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 @@
@@ -47,9 +47,9 @@
Сводка
- Workforce
- Security
- Forensics
+ Работа сотрудников
+ Риски и безопасность
+ Расследования
Отчеты
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` включает темную тему.
+- раздел рисков включает темную тему.