fix(portal): use management language in navigation

This commit is contained in:
igor04091968
2026-06-04 07:09:30 +03:00
parent 8b621a62d6
commit 4197efe482
3 changed files with 176 additions and 114 deletions
+155 -93
View File
@@ -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]) => `
<article class="card">
<span class="badge ${statusClass(block.status)}">${escapeHtml(block.status)}</span>
<h3>${escapeHtml(label(name))}</h3>
<p class="muted">${escapeHtml(block.text)}</p>
<h3>${ui(label(name))}</h3>
<p class="muted">${ui(block.text)}</p>
</article>
`).join("");
}
@@ -71,14 +133,14 @@ function renderReadinessSummaryCard(readiness) {
<span class="badge ${statusClass(status)}">${escapeHtml(status)}</span>
<h3>Готовность системы</h3>
</div>
<button class="small-button" data-readiness-verify="true">Проверить bundle</button>
<button class="small-button" data-readiness-verify="true">Проверить пакет</button>
</div>
<div class="readiness-metrics">
<div><span class="muted">Дата</span><strong>${escapeHtml(generated)}</strong></div>
<div><span class="muted">Подпись</span><strong class="${signatureOk ? "text-ok" : "text-fail"}">${signatureOk ? "OK" : "FAIL"}</strong></div>
<div><span class="muted">Checksum</span><strong class="${checksumOk ? "text-ok" : "text-fail"}">${checksumOk ? "OK" : "FAIL"}</strong></div>
<div><span class="muted">Контрольная сумма</span><strong class="${checksumOk ? "text-ok" : "text-fail"}">${checksumOk ? "OK" : "FAIL"}</strong></div>
</div>
<p class="muted small">Отпечаток: <code>${escapeHtml(shortFingerprint(fingerprint))}</code></p>
<p class="muted small">Отпечаток ключа: <code>${escapeHtml(shortFingerprint(fingerprint))}</code></p>
<p id="readinessVerifyStatus" class="muted small">${escapeHtml(verificationText)}</p>
</article>
`;
@@ -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 `
<article class="metric-card">
<span class="badge ${statusClass(status)}">${escapeHtml(status || "INFO")}</span>
<h3>${escapeHtml(labelText)}</h3>
<p class="metric-value">${escapeHtml(value ?? "Нет данных")}</p>
<p class="muted">${escapeHtml(context || "")}</p>
<h3>${ui(labelText)}</h3>
<p class="metric-value">${ui(value ?? "Нет данных")}</p>
<p class="muted">${ui(context || "")}</p>
</article>
`;
}
@@ -133,11 +195,11 @@ function renderExecutiveMetrics(report, incidents) {
const openCount = Array.isArray(incidents) ? incidents.filter(item => !item.acknowledged).length : 0;
return `
<section class="executive-grid" aria-label="Управленческая сводка">
${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 `<div class="list compact-list">${items.slice(0, 12).map(item => `
<div class="row compact-row">
<strong>${escapeHtml(item.label || "-")}</strong>
<span class="muted">${escapeHtml(item.value || "")}</span>
<strong>${ui(item.label || "-")}</strong>
<span class="muted">${ui(item.value || "")}</span>
<span class="badge ${statusClass(item.status)}">${escapeHtml(item.status || "INFO")}</span>
</div>
`).join("")}</div>`;
@@ -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 `<div class="report-type-grid">${types.map(([title, text, status]) => `
<article class="report-type">
<span class="badge ${statusClass(status)}">${escapeHtml(status)}</span>
<h3>${escapeHtml(title)}</h3>
<p class="muted">${escapeHtml(text)}</p>
<h3>${ui(title)}</h3>
<p class="muted">${ui(text)}</p>
</article>
`).join("")}</div>`;
}
@@ -195,9 +257,9 @@ function renderReportTypeCards() {
function renderLinks(links) {
const link = (text, href) => `<a class="button" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
return `<div class="links">
${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) => `<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_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)}
</div>`;
}
@@ -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 `<div class="list">${sources.map(([name, source]) => `
<div class="row">
<strong>${escapeHtml(name)}</strong>
<span class="muted">${escapeHtml(source?.summary || source?.error || "нет данных")}</span>
<strong>${ui(name)}</strong>
<span class="muted">${ui(source?.summary || source?.error || "нет данных")}</span>
<span class="badge ${statusClass(source?.status || source?.ok)}">${escapeHtml(source?.status || (source?.ok ? "OK" : "FAIL"))}</span>
</div>
`).join("")}</div>`;
@@ -247,8 +309,8 @@ function renderOperator(data, report) {
${renderExecutiveMetrics(report, incidents)}
<section class="dashboard-band">
<div class="band-head"><h3>Рабочая активность сотрудников</h3><span class="muted">загрузка, простои, перегруз и дисциплина процессов</span></div>
${renderSectionItems(workforce, "Workforce-срез пока не сформирован.")}
${renderSectionItems(insights, "Отклонений Workforce пока не найдено.")}
${renderSectionItems(workforce, "Срез по работе сотрудников пока не сформирован.")}
${renderSectionItems(insights, "Отклонений по работе сотрудников пока не найдено.")}
</section>
<section class="dashboard-band security-band">
<div class="band-head"><h3>Контроль безопасности</h3><span class="muted">подозрительные события и приоритет реакции</span></div>
@@ -280,7 +342,7 @@ function renderManager(data, policyExplain) {
<section class="card">
<h3>Индекс активности</h3>
<p class="kpi-value">${escapeHtml(workforceIndex)}</p>
<p class="muted">proxy: активное время / плановое рабочее время</p>
<p class="muted">расчет: активное время / плановое рабочее время</p>
<p class="muted">Сотрудников: ${data.users_count}; активных часов: ${Number(data.total_active_hours || 0).toFixed(1)}</p>
<p class="muted">${escapeHtml(data.status?.text || "")}</p>
</section>
@@ -334,7 +396,7 @@ function renderWorkforceIndexExplanation(policy) {
return `
<section class="card index-explain-card">
<h3>Почему такой индекс?</h3>
<p class="muted">Role/application policy не настроена. Доступен только нейтральный индекс активности.</p>
<p class="muted">Правила ролей и приложений не настроены. Доступен только нейтральный индекс активности.</p>
</section>
`;
}
@@ -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
? `<div class="row compact-row"><strong>Нет приложений</strong><span class="muted">Нет top breakdown для weighted KPI</span><span></span></div>`
? `<div class="row compact-row"><strong>Нет приложений</strong><span class="muted">Нет разбора по приложениям для взвешенного показателя</span><span></span></div>`
: details.map(item => {
const contribution = Math.round(Number(item.weighted_seconds || 0) / weightedTotal * 100);
return `
<div class="row app-weight-row">
<strong>${escapeHtml(item.application || "-")}</strong>
<span class="muted">${escapeHtml(humanSeconds(item.seconds))} · вес ${escapeHtml(pctText(item.weight))} · правило ${escapeHtml(item.matched_rule || "default_weight")}</span>
<span class="muted">${escapeHtml(humanSeconds(item.seconds))} · вес ${escapeHtml(pctText(item.weight))} · правило ${ui(item.matched_rule || "нет явного правила")}</span>
<span class="badge ${Number(item.weight || 0) > 0 ? "status-ok" : "status-unknown"}">${escapeHtml(humanSeconds(item.weighted_seconds))} · ${contribution}%</span>
</div>
`;
@@ -358,16 +420,16 @@ function renderWorkforceIndexExplanation(policy) {
<div class="section-head">
<div>
<h3>Почему такой индекс?</h3>
<p class="muted">${escapeHtml(policy.explanation || "Индекс = взвешенное время приложений / плановое время роли.")}</p>
<p class="muted small">Формула: ${escapeHtml(policy.formula || "index = weighted_seconds / planned_seconds × 100")}.</p>
<p class="muted">${ui(policy.explanation || "Индекс = взвешенное время приложений / плановое время роли.")}</p>
<p class="muted small">Формула: индекс = время с учетом правил / плановое время × 100.</p>
</div>
<span class="badge ${statusClass(workforceIndexStatus(policy.index))}">${escapeHtml(workforceIndexTextFromValue(policy.index))}</span>
</div>
<div class="index-metrics">
<div><span class="muted">Роль</span><strong>${escapeHtml(policy.role_label || policy.role || "-")}</strong></div>
<div><span class="muted">План</span><strong>${escapeHtml(humanSeconds(policy.planned_seconds))}</strong></div>
<div><span class="muted">App time</span><strong>${escapeHtml(humanSeconds(policy.app_seconds))}</strong></div>
<div><span class="muted">Weighted</span><strong>${escapeHtml(humanSeconds(policy.weighted_seconds))}</strong></div>
<div><span class="muted">Время приложений</span><strong>${escapeHtml(humanSeconds(policy.app_seconds))}</strong></div>
<div><span class="muted">С учетом правил</span><strong>${escapeHtml(humanSeconds(policy.weighted_seconds))}</strong></div>
</div>
<div class="list compact-list app-weight-list">${appRows}</div>
${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 `<div class="audit-note"><strong>Аудит policy</strong><span class="muted">Все top приложения попали под явные правила или данных для аудита нет.</span></div>`;
return `<div class="audit-note"><strong>Проверка правил</strong><span class="muted">Ключевые приложения попали под явные правила или данных для проверки нет.</span></div>`;
}
return `
<div class="audit-block">
<h4>Аудит policy: default_weight</h4>
<h4>Проверка правил: приложения без явного правила</h4>
<p class="muted small">Эти приложения не нашли явного правила и требуют проверки классификации.</p>
<div class="list compact-list">${items.map(item => `
<div class="row compact-row">
<strong>${escapeHtml(item.application || "-")}</strong>
<span class="muted">${escapeHtml(humanSeconds(item.seconds))} · default ${escapeHtml(pctText(item.weight))}</span>
<span class="badge status-warn">review</span>
<span class="muted">${escapeHtml(humanSeconds(item.seconds))} · вес по умолчанию ${escapeHtml(pctText(item.weight))}</span>
<span class="badge status-warn">проверить</span>
</div>
`).join("")}</div>
</div>
@@ -400,12 +462,12 @@ function renderEmployeeIndexDetails(items) {
if (!items.length) return "";
return `
<div class="employee-drilldown">
<h4>Drill-down по сотрудникам</h4>
<p class="muted small">Это не персональный weighted KPI: per-user индекс сейчас считается по активному времени; app-weight breakdown доступен только на уровне портфеля.</p>
<h4>Разбор по сотрудникам</h4>
<p class="muted small">Это не персональный взвешенный показатель: индекс сотрудника сейчас считается по активному времени; разбор по весам приложений доступен только на уровне общего среза.</p>
<div class="list compact-list">${items.map(item => `
<div class="row employee-index-row">
<strong>${escapeHtml(item.user || "-")}</strong>
<span class="muted">${escapeHtml(item.reason || `${item.formula || "employee_index = active_seconds / planned_seconds × 100"} · active ${humanSeconds(item.active_seconds)} / plan ${humanSeconds(item.planned_seconds)}`)}</span>
<span class="muted">${ui(item.reason || `индекс сотрудника = активное время / плановое время × 100 · активно ${humanSeconds(item.active_seconds)} / план ${humanSeconds(item.planned_seconds)}`)}</span>
<span class="badge ${statusClass(item.status)}">${escapeHtml(workforceIndexTextFromValue(item.index))}</span>
</div>
`).join("")}</div>
@@ -432,23 +494,23 @@ function renderOwner(data) {
<div class="page-head">
<div>
<h2 class="section-title">Контроль безопасности</h2>
<p class="muted">Приоритеты ИБ: подозрительные подключения, DLP-сигналы, RDP-аномалии и состояние периметра.</p>
<p class="muted">Приоритеты ИБ: подозрительные подключения, сигналы по данным, RDP-аномалии и состояние периметра.</p>
</div>
<span class="badge ${statusClass(data.summary?.severity)}">${escapeHtml(data.summary?.severity || "INFO")}</span>
</div>
<div class="summary-grid">${cards.map(([name, block]) => `
<article class="card">
<span class="badge ${statusClass(block.status)}">${escapeHtml(block.status)}</span>
<h3>${escapeHtml(label(name))}</h3>
<p class="muted">${escapeHtml(block.text)}</p>
<h3>${ui(label(name))}</h3>
<p class="muted">${ui(block.text)}</p>
</article>
`).join("")}</div>
<section class="card">
<h3>Риски с приоритетом</h3>
<div class="list">${(data.recommendations || []).map(item => `<div class="row"><strong>Рекомендация</strong><span class="muted">${escapeHtml(item)}</span><span></span></div>`).join("")}</div>
<div class="list">${(data.recommendations || []).map(item => `<div class="row"><strong>Рекомендация</strong><span class="muted">${ui(item)}</span><span></span></div>`).join("")}</div>
</section>
<section class="card">
<h3>Дашборды безопасности</h3>
<h3>Графики безопасности</h3>
${renderLinks(data.links)}
</section>
`;
@@ -459,12 +521,12 @@ function renderIncidentsList(items) {
return `<div class="list">${items.map(item => `
<div class="row incident-row">
<div>
<strong>${escapeHtml(item.source)}</strong>
<div class="muted small">${escapeHtml(item.kind)} · ${escapeHtml(item.id)}</div>
<strong>${ui(item.source)}</strong>
<div class="muted small">${ui(item.kind)} · ${escapeHtml(item.id)}</div>
</div>
<div>
<span class="muted">${escapeHtml(item.summary)}</span>
${item.acknowledged ? `<div class="muted small">В работе: ${escapeHtml(item.assigned_to || item.actor || "оператор")} · ${escapeHtml(item.comment || "")}</div>` : ""}
<span class="muted">${ui(item.summary)}</span>
${item.acknowledged ? `<div class="muted small">В работе: ${ui(item.assigned_to || item.actor || "оператор")} · ${ui(item.comment || "")}</div>` : ""}
</div>
<span class="badge ${statusClass(item.status)}">${escapeHtml(item.status)}</span>
<div class="actions">
@@ -482,15 +544,15 @@ function isDlpIncident(item) {
function renderDlpIncidentsList(items) {
const dlpItems = (items || []).filter(isDlpIncident);
if (dlpItems.length === 0) return `<p class="muted">Активных DLP/ИБ-инцидентов нет.</p>`;
if (dlpItems.length === 0) return `<p class="muted">Активных ИБ-инцидентов по данным нет.</p>`;
return renderIncidentsList(dlpItems);
}
function renderDlpEvidence(evidence) {
if (!evidence) return `<p class="muted">Данные evidence загружаются.</p>`;
if (!evidence.ok) return `<p class="muted">Evidence недоступны: ${escapeHtml(evidence.error || "ошибка чтения")}</p>`;
if (!evidence) return `<p class="muted">Материалы проверки загружаются.</p>`;
if (!evidence.ok) return `<p class="muted">Материалы недоступны: ${ui(evidence.error || "ошибка чтения")}</p>`;
const items = evidence.items || [];
if (items.length === 0) return `<p class="muted">DLP evidence пока не найдены.</p>`;
if (items.length === 0) return `<p class="muted">Материалы по ИБ-инцидентам пока не найдены.</p>`;
return `<div class="list evidence-list">${items.map(item => `
<div class="row evidence-row">
<div>
@@ -499,7 +561,7 @@ function renderDlpEvidence(evidence) {
</div>
<div>
<span class="muted">${escapeHtml(item.message || item.file_path || item.rule_id || item.event_id)}</span>
<div class="muted small">${item.source_file ? "Файл: " + escapeHtml(item.source_file) + " · " : ""}${item.screenshot_sha256 ? "SHA-256: " + escapeHtml(item.screenshot_sha256) : escapeHtml(item.blocked_reason || "без скрина")}</div>
<div class="muted small">${item.source_file ? "Файл: " + escapeHtml(item.source_file) + " · " : ""}${item.screenshot_sha256 ? "Контрольный хеш: " + escapeHtml(item.screenshot_sha256) : ui(item.blocked_reason || "без скрина")}</div>
</div>
<span class="badge ${item.screenshot_available ? "status-ok" : "status-warn"}">${item.screenshot_available ? "СКРИН" : "МЕТА"}</span>
<div class="actions">
@@ -518,9 +580,9 @@ function renderIncidents(data) {
<div class="page-head">
<div>
<h2 class="section-title">Расследования и доказательная база</h2>
<p class="muted">Кто, когда, откуда, куда, связанные хосты, evidence и выгрузка материалов.</p>
<p class="muted">Кто, когда, откуда, куда, связанные хосты, материалы и выгрузка для проверки.</p>
</div>
<span class="badge ${statusClass(incidentStatusFromCount((incidents || []).length))}">${escapeHtml((incidents || []).length)} items</span>
<span class="badge ${statusClass(incidentStatusFromCount((incidents || []).length))}">${escapeHtml((incidents || []).length)} записей</span>
</div>
<div class="grid-2">
<section class="card">
@@ -528,12 +590,12 @@ function renderIncidents(data) {
${renderDlpIncidentsList(incidents)}
</section>
<section class="card">
<h3>Связанные графики и дашборды</h3>
<h3>Связанные графики</h3>
${renderDlpLinks(links)}
</section>
</div>
<section class="card evidence-card">
<h3>Evidence: скриншоты, хеши, файлы</h3>
<h3>Материалы: скриншоты, хеши, файлы</h3>
${renderDlpEvidence(evidence)}
</section>
`;
@@ -543,9 +605,9 @@ function renderKpiCards(items) {
return `<div class="summary-grid kpi-grid">${(items || []).map(item => `
<article class="card kpi-card">
<span class="badge ${statusClass(item.status)}">${escapeHtml(item.status || "INFO")}</span>
<h3>${escapeHtml(item.label)}</h3>
<p class="kpi-value">${escapeHtml(item.value)}</p>
<p class="muted">${escapeHtml(item.context || "")}</p>
<h3>${ui(item.label)}</h3>
<p class="kpi-value">${ui(item.value)}</p>
<p class="muted">${ui(item.context || "")}</p>
</article>
`).join("")}</div>`;
}
@@ -553,11 +615,11 @@ function renderKpiCards(items) {
function renderReportSections(sections) {
return `<div class="grid-2">${(sections || []).map(section => `
<section class="card report-section">
<h3>${escapeHtml(section.title)}</h3>
<h3>${ui(section.title)}</h3>
<div class="list compact-list">${(section.items || []).map(item => `
<div class="row compact-row">
<strong>${escapeHtml(item.label)}</strong>
<span class="muted">${escapeHtml(item.value)}</span>
<strong>${ui(item.label)}</strong>
<span class="muted">${ui(item.value)}</span>
<span class="badge ${statusClass(item.status)}">${escapeHtml(item.status || "INFO")}</span>
</div>
`).join("")}</div>
@@ -575,21 +637,21 @@ function renderUebaRisk(risk) {
<section class="card ueba-risk-card">
<div class="section-head">
<div>
<h3>UEBA риск</h3>
<p class="muted">${escapeHtml(risk.note || "Read-only risk score без автоматического воздействия.")}</p>
<p class="muted small">Формула: ${escapeHtml(risk.formula || "sum(reason_points) capped at 100")}.</p>
<p class="muted small">Confidence: ${escapeHtml(confidence)} · sources: ${escapeHtml(sources)} · baseline: ${escapeHtml(risk.baseline_status || "-")} · policy: ${escapeHtml(risk.policy_version || "-")}</p>
<p class="muted small">Baseline window: ${escapeHtml(risk.baseline_window_days || "-")} days · available: ${escapeHtml(baselineReady)} · deviation: ${escapeHtml(risk.deviation_score ?? 0)}</p>
<h3>Индекс риска</h3>
<p class="muted">${ui(risk.note || "Оценка риска без автоматического воздействия.")}</p>
<p class="muted small">Формула: сумма факторов риска, максимум 100.</p>
<p class="muted small">Достоверность: ${escapeHtml(confidence)} · источники: ${ui(sources)} · обычный профиль: ${ui(risk.baseline_status || "-")} · версия правил: ${ui(risk.policy_version || "-")}</p>
<p class="muted small">Окно сравнения: ${escapeHtml(risk.baseline_window_days || "-")} дн. · доступно: ${ui(baselineReady)} · отклонение: ${escapeHtml(risk.deviation_score ?? 0)}</p>
</div>
<span class="badge ${statusClass(risk.status)}">${escapeHtml(risk.level || "unknown")} · ${escapeHtml(risk.score ?? 0)}/100</span>
</div>
<div class="list compact-list">${reasons.length ? reasons.map(item => `
<div class="row compact-row">
<strong>${escapeHtml(item.label || item.code || "-")}</strong>
<span class="muted">${escapeHtml(item.value || "")} · ${escapeHtml(item.recommendation || "")}</span>
<strong>${ui(item.label || item.code || "-")}</strong>
<span class="muted">${ui(item.value || "")} · ${ui(item.recommendation || "")}</span>
<span class="badge ${statusClass(item.status || item.severity)}">+${escapeHtml(item.points || 0)}</span>
</div>
`).join("") : `<div class="row compact-row"><strong>Сигналы</strong><span class="muted">Существенных UEBA-сигналов в текущем срезе нет.</span><span class="badge status-ok">OK</span></div>`}</div>
`).join("") : `<div class="row compact-row"><strong>Сигналы</strong><span class="muted">Существенных риск-сигналов в текущем срезе нет.</span><span class="badge status-ok">OK</span></div>`}</div>
</section>
`;
}
@@ -608,21 +670,21 @@ function renderReports(data) {
<section class="card report-hero">
<span class="badge ${statusClass(data.severity)}">${escapeHtml(data.severity)}</span>
${data.anonymized ? `<span class="badge status-warn">обезличено</span>` : ""}
<h3>${escapeHtml(data.headline)}</h3>
<p class="muted">${escapeHtml(data.period || "")} · обновлено ${escapeHtml(data.generated_at_utc || "")}</p>
<h3>${ui(data.headline)}</h3>
<p class="muted">${ui(data.period || "")} · обновлено ${escapeHtml(data.generated_at_utc || "")}</p>
</section>
<section class="card">
<h3>Для руководителя</h3>
<div class="list compact-list">${(data.executive_points || []).map(point => `
<div class="row compact-row"><strong>Итог</strong><span class="muted">${escapeHtml(point)}</span><span></span></div>
<div class="row compact-row"><strong>Итог</strong><span class="muted">${ui(point)}</span><span></span></div>
`).join("")}</div>
</section>
</div>
<div class="report-actions">
<button class="small-button" data-anonymize-report="true">Демо без имен</button>
<button class="small-button" data-export-markdown="true">Markdown</button>
<button class="small-button" data-export-markdown="true">Текстовый отчет</button>
<button class="small-button" data-print-report="true">Печать / PDF</button>
<a class="small-button" href="${apiBase()}/reports" target="_blank" rel="noopener noreferrer">JSON</a>
<a class="small-button" href="${apiBase()}/reports" target="_blank" rel="noopener noreferrer">Скачать данные</a>
</div>
<h3 class="section-title">Ключевые показатели</h3>
${renderKpiCards(data.kpis)}
@@ -631,8 +693,8 @@ function renderReports(data) {
<h3 class="section-title">Срезы отчета</h3>
${renderReportSections(data.sections)}
<section class="card markdown-card">
<h3>Markdown для отчета</h3>
<pre>${escapeHtml(data.markdown || "")}</pre>
<h3>Текст для отчета</h3>
<pre>${ui(data.markdown || "")}</pre>
</section>
`;
}
@@ -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");
@@ -11,7 +11,7 @@
<header class="topbar">
<div class="brand">
<p class="eyebrow">AWatch-rus</p>
<h1>Workforce / Security / Forensics</h1>
<h1>Активность / Риски / Расследования</h1>
<p class="lead">Рабочий кабинет руководителя, ИБ и расследователя</p>
</div>
<div class="topbar-side" aria-label="Параметры отчета">
@@ -38,8 +38,8 @@
</label>
</div>
<div class="header-actions">
<button class="small-button" data-export-markdown="true">Markdown</button>
<button class="small-button" data-print-report="true">PDF</button>
<button class="small-button" data-export-markdown="true">Текстовый отчет</button>
<button class="small-button" data-print-report="true">Печать / PDF</button>
<div id="globalStatus" class="status-pill status-unknown">Загрузка</div>
</div>
</div>
@@ -47,9 +47,9 @@
<nav class="tabs" aria-label="Разделы портала">
<button class="tab is-active" data-tab="operator">Сводка</button>
<button class="tab" data-tab="manager">Workforce</button>
<button class="tab" data-tab="owner">Security</button>
<button class="tab" data-tab="incidents">Forensics</button>
<button class="tab" data-tab="manager">Работа сотрудников</button>
<button class="tab" data-tab="owner">Риски и безопасность</button>
<button class="tab" data-tab="incidents">Расследования</button>
<button class="tab" data-tab="reports">Отчеты</button>
</nav>