diff --git a/adk-rust/crates/detmir-portal/src/static/app.css b/adk-rust/crates/detmir-portal/src/static/app.css index 726efdf..d47d146 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.css +++ b/adk-rust/crates/detmir-portal/src/static/app.css @@ -439,6 +439,25 @@ h1 { grid-template-columns: minmax(140px, 1fr) minmax(280px, 2fr) auto; } +.employee-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; + margin-bottom: 12px; +} + +.employee-card { + min-height: 180px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); +} + +.employee-metrics { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .report-actions { display: flex; justify-content: flex-end; @@ -550,6 +569,7 @@ pre { .readiness-metrics { grid-template-columns: 1fr; } .app-weight-row { grid-template-columns: 1fr; } .employee-index-row { grid-template-columns: 1fr; } + .employee-metrics { grid-template-columns: 1fr; } .section-head { flex-direction: column; } .actions { justify-content: flex-start; } } diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index cdce4ae..131cd1c 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -369,6 +369,84 @@ function renderManager(data, policyExplain) { `; } +function renderDepartments(report) { + const departments = report?.workforce?.department_comparison || []; + const owners = report?.workforce?.owner_comparison || []; + const insights = report?.workforce?.insights || []; + return ` +
+
+

Подразделения

+

Сравнение активности, тренда, риска и ответственных по группам.

+
+ ${ui(report?.severity || "INFO")} +
+
+
+

Рейтинг подразделений

+ ${renderSimpleItems(departments, "Подразделения пока не рассчитаны.")} +
+
+

Ответственные

+ ${renderSimpleItems(owners, "Ответственные пока не рассчитаны.")} +
+
+
+

Просадки и отклонения

что требует управленческого внимания
+ ${renderSimpleItems(insights, "Существенных отклонений по подразделениям пока нет.")} +
+ `; +} + +function renderEmployees(data, policyExplain) { + const employees = Array.isArray(policyExplain?.employee_details) ? policyExplain.employee_details : []; + const users = Array.isArray(data.users) ? data.users : []; + const selected = users.slice(0, 12); + return ` +
+
+

Карточки сотрудников

+

Рабочий день, приложения, активность, последнее событие и риск-сигналы.

+
+ ${escapeHtml(data.users_count || 0)} сотрудников +
+
${selected.map(user => renderEmployeeCard(user, employees)).join("") || `

Сотрудники пока не найдены.

`}
+ ${renderWorkforceIndexExplanation(policyExplain)} + `; +} + +function renderEmployeeCard(user, employeeDetails) { + const detail = employeeDetails.find(item => item.user === user.user) || {}; + return ` +
+
+
+

${ui(user.user || "Сотрудник")}

+

Последняя активность: ${ui(user.last_activity || "-")}

+
+ ${ui(workforceIndexTextFromValue(detail.index))} +
+
+
Активность${ui(user.active_hhmm || "00:00")}
+
Сессии${escapeHtml(user.sessions_count || 0)}
+
План${escapeHtml(humanSeconds(detail.planned_seconds))}
+
+

${ui(detail.reason || "Персональный разбор по приложениям будет точнее после накопления данных по ролям.")}

+
+ `; +} + +function renderSimpleItems(items, emptyText) { + if (!Array.isArray(items) || items.length === 0) return `

${ui(emptyText)}

`; + return `
${items.slice(0, 16).map(item => ` +
+ ${ui(item.label || item.title || "-")} + ${ui(item.value || item.subject || "")} + ${ui(item.status || "INFO")} +
+ `).join("")}
`; +} + function workforceIndexText(usersCount, activeSeconds) { const users = Number(usersCount || 0); const seconds = Number(activeSeconds || 0); @@ -516,6 +594,30 @@ function renderOwner(data) { `; } +function renderPerimeter(data, report) { + const risk = report?.ueba_risk || {}; + const cards = Object.entries(data.cards || {}); + return ` +
+
+

Сетевой периметр

+

Режим наблюдения: состояние внешних сигналов, необычные направления и связь с рисками сотрудников. Управление сетью отсюда не выполняется.

+
+ наблюдение +
+
+ ${metricCard("Нетипичные направления", "нет данных", "UNKNOWN", "источник периметра не подключен к порталу")} + ${metricCard("Сетевые события", "нет данных", "UNKNOWN", "ожидается интеграционный слой")} + ${metricCard("Связь с рисками", `${risk.score ?? 0}/100`, risk.status || "UNKNOWN", risk.summary || "оценка по правилам")} + ${metricCard("Автоматическое воздействие", "выключено", "OK", "портал работает только на чтение")} +
+
+

Состояние доступных сигналов

без изменения правил периметра
+ ${renderSimpleItems(cards.map(([name, block]) => ({ label: label(name), value: block.text, status: block.status })), "Сигналы периметра пока не подключены.")} +
+ `; +} + function renderIncidentsList(items) { if (!items || items.length === 0) return `

Активных проблем нет.

`; return `
${items.map(item => ` @@ -708,22 +810,45 @@ async function refresh() { }; renderSummary(summary, state.readiness); const content = document.getElementById("content"); - const data = await loadJson(`/${state.tab}`); if (state.tab === "operator") { + const data = await loadJson("/operator"); state.reports = await loadJson("/reports").catch(() => state.reports); content.innerHTML = renderOperator(data, state.reports); updateFilters(state.reports); } if (state.tab === "manager") { + const data = await loadJson("/manager"); const policyExplain = await loadJson("/workforce/policy/explain").catch(() => null); content.innerHTML = renderManager(data, policyExplain); } - if (state.tab === "owner") content.innerHTML = renderOwner(data); + if (state.tab === "departments") { + const data = await loadJson("/reports"); + state.reports = data; + content.innerHTML = renderDepartments(data); + updateFilters(data); + } + if (state.tab === "employees") { + const data = await loadJson("/manager"); + const policyExplain = await loadJson("/workforce/policy/explain").catch(() => null); + content.innerHTML = renderEmployees(data, policyExplain); + } + if (state.tab === "owner") { + const data = await loadJson("/owner"); + content.innerHTML = renderOwner(data); + } if (state.tab === "incidents") { + const data = await loadJson("/incidents"); const evidence = await loadJson("/dlp/evidence").catch(error => ({ ok: false, error: error.message, items: [] })); content.innerHTML = renderIncidents({ incidents: data, evidence }); } + if (state.tab === "perimeter") { + const data = await loadJson("/owner"); + const reports = await loadJson("/reports").catch(() => state.reports); + state.reports = reports; + content.innerHTML = renderPerimeter(data, reports); + } if (state.tab === "reports") { + const data = await loadJson("/reports"); state.reports = data; content.innerHTML = renderReports(data); updateFilters(data); @@ -741,7 +866,7 @@ function setTab(tab) { } function applySecurityMode(tab) { - document.body.classList.toggle("security-mode", tab === "owner" || tab === "incidents"); + document.body.classList.toggle("security-mode", tab === "owner" || tab === "incidents" || tab === "perimeter"); } function showError(error) { diff --git a/adk-rust/crates/detmir-portal/src/static/index.html b/adk-rust/crates/detmir-portal/src/static/index.html index ff4d15c..d171b02 100644 --- a/adk-rust/crates/detmir-portal/src/static/index.html +++ b/adk-rust/crates/detmir-portal/src/static/index.html @@ -46,10 +46,13 @@ diff --git a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md index 138ef04..078e7ee 100644 --- a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md +++ b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md @@ -15,11 +15,18 @@ - Верхняя панель переименована в `AWatch-rus Активность / Риски / Расследования`. - Добавлены элементы отчета: период, подразделение, ответственный, текстовый отчет/PDF, статус сбора данных. - Навигация приведена к продуктовой модели: - - `Сводка`; - - `Работа сотрудников`; - - `Риски и безопасность`; + - `Пульс организации`; + - `Рабочее время`; + - `Подразделения`; + - `Сотрудники`; + - `Риски`; - `Расследования`; + - `Сетевой периметр`; - `Отчеты`. +- Добавлены отдельные промышленные представления: + - экран подразделений: рейтинг групп, ответственные, просадки и отклонения; + - экран сотрудников: карточка сотрудника, активность, сессии, плановое время, последнее событие; + - экран сетевого периметра: режим наблюдения без управления сетевыми правилами. - Первый экран показывает executive summary: - индекс активности за день; - активные сотрудники; @@ -39,6 +46,7 @@ - Фильтры периода, подразделения и ответственного пока являются UI-контролами. Backend-фильтрация должна добавляться отдельным изменением, чтобы не искажать текущие данные. - `Индекс активности` остается расчетной метрикой: активное время / плановое время. Взвешенный показатель объясняется отдельно через правила ролей и приложений. - Разделы рисков и расследований показывают риск-срез и материалы проверки. Это не заявляется как сертифицированная СЗИ. +- Раздел сетевого периметра работает как витрина наблюдения. Он не меняет правила сети и не выполняет блокировки. - pfSense и сетевой периметр этим изменением не затрагивались. ## Проверки @@ -73,7 +81,7 @@ cargo build --release --manifest-path adk-rust/Cargo.toml -p detmir-portal Playwright smoke: - заголовок страницы: `AWatch-rus Portal`; -- вкладки `Сводка`, `Работа сотрудников`, `Риски и безопасность`, `Расследования`, `Отчеты` открываются; +- вкладки `Пульс организации`, `Рабочее время`, `Подразделения`, `Сотрудники`, `Риски`, `Расследования`, `Сетевой периметр`, `Отчеты` открываются; - JS-ошибок нет; - API-ответов с кодом 400+ нет; - раздел рисков включает темную тему.