From 2645082183f3c509e1cbadf629bccf7b42416499 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Thu, 4 Jun 2026 07:50:08 +0300 Subject: [PATCH] feat(portal): apply industrial workforce dashboard layout --- .../crates/detmir-portal/src/static/app.css | 230 +++++++++++++++--- .../crates/detmir-portal/src/static/app.js | 152 ++++++++++-- .../detmir-portal/src/static/index.html | 102 ++++---- docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md | 26 +- 4 files changed, 405 insertions(+), 105 deletions(-) diff --git a/adk-rust/crates/detmir-portal/src/static/app.css b/adk-rust/crates/detmir-portal/src/static/app.css index 4ea40d4..7dde6aa 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.css +++ b/adk-rust/crates/detmir-portal/src/static/app.css @@ -1,21 +1,23 @@ :root { color-scheme: light; - --bg: #f4f6f8; + --bg: #f8fafc; --panel: #ffffff; - --ink: #172026; - --muted: #5d6872; - --line: #d8dee5; - --ok: #167347; - --ok-bg: #e5f4ec; - --warn: #9a6500; - --warn-bg: #fff2cc; - --fail: #b42318; - --fail-bg: #fde7e5; - --unknown: #59636e; - --unknown-bg: #e9edf1; - --link: #075985; - --accent: #234b62; + --ink: #0f172a; + --muted: #64748b; + --line: #e2e8f0; + --ok: #16a34a; + --ok-bg: #dcfce7; + --warn: #f97316; + --warn-bg: #ffedd5; + --fail: #dc2626; + --fail-bg: #fee2e2; + --unknown: #64748b; + --unknown-bg: #f1f5f9; + --link: #2563eb; + --accent: #2563eb; --soft: #f8fafc; + --sidebar: #0f172a; + --sidebar-muted: #94a3b8; } * { box-sizing: border-box; } @@ -27,6 +29,40 @@ body { font-family: Inter, "Segoe UI", Arial, sans-serif; } +.app-layout { + display: grid; + grid-template-columns: 248px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + min-height: 100vh; + padding: 18px 14px; + background: var(--sidebar); + color: #e2e8f0; +} + +.sidebar-brand { + display: grid; + gap: 4px; + padding: 8px 10px 18px; + border-bottom: 1px solid rgba(148, 163, 184, 0.24); + margin-bottom: 12px; +} + +.sidebar-brand strong { + font-size: 18px; +} + +.sidebar-brand span { + color: var(--sidebar-muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + body.security-mode { --bg: #111827; --panel: #172026; @@ -54,9 +90,9 @@ body.security-mode .tab.is-active { } .shell { - max-width: 1440px; - margin: 0 auto; - padding: 22px; + max-width: none; + min-width: 0; + padding: 20px 22px 28px; } .topbar { @@ -65,6 +101,11 @@ body.security-mode .tab.is-active { justify-content: space-between; gap: 16px; margin-bottom: 16px; + padding: 16px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05); } .brand { @@ -127,27 +168,32 @@ h1 { } .tabs { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 16px; + display: grid; + gap: 6px; } .tab { - border: 1px solid var(--line); - background: var(--panel); - color: var(--ink); + width: 100%; + border: 1px solid transparent; + background: transparent; + color: #cbd5e1; border-radius: 8px; - padding: 10px 14px; + padding: 11px 12px; font: inherit; font-weight: 700; + text-align: left; cursor: pointer; } .tab.is-active { - border-color: var(--link); - color: var(--link); - background: #eef7fb; + border-color: rgba(37, 99, 235, 0.7); + color: #ffffff; + background: #1d4ed8; +} + +.tab:hover { + background: rgba(148, 163, 184, 0.14); + color: #ffffff; } .summary-grid { @@ -157,11 +203,17 @@ h1 { margin-bottom: 16px; } +.service-summary { + margin-top: 16px; + margin-bottom: 0; +} + .card, .content-panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); } .card { @@ -297,33 +349,123 @@ h1 { .executive-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); - gap: 10px; - margin-bottom: 14px; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 16px; } .metric-card { - min-height: 132px; + min-height: 136px; padding: 14px; border: 1px solid var(--line); - border-left: 4px solid var(--accent); + border-top: 3px solid var(--accent); border-radius: 8px; background: var(--panel); } -.metric-card h3, +.kpi-label, +.metric-card h3 { + display: block; + margin: 8px 0 6px; + color: var(--muted); + font-size: 13px; + font-weight: 800; + text-transform: uppercase; +} + .report-type h3 { margin: 10px 0 6px; font-size: 15px; } +.kpi-trend { + display: block; + color: var(--muted); + font-size: 13px; + font-weight: 700; +} + +.kpi-trend.positive { color: var(--ok); } +.kpi-trend.warning { color: var(--warn); } +.kpi-trend.negative { color: var(--fail); } +.kpi-trend.ok { color: var(--ok); } +.kpi-trend.warn { color: var(--warn); } +.kpi-trend.fail { color: var(--fail); } + .metric-value { + display: block; margin: 8px 0 4px; - font-size: 26px; + font-size: 32px; font-weight: 800; line-height: 1.1; } +.analytics-grid { + display: grid; + grid-template-columns: minmax(280px, 1fr) minmax(280px, 1fr) minmax(280px, 1fr); + gap: 12px; + margin-bottom: 14px; +} + +.analytics-panel { + min-height: 260px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.data-table th, +.data-table td { + padding: 9px 8px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; +} + +.data-table th { + color: var(--muted); + font-size: 12px; + text-transform: uppercase; +} + +.rank-list { + display: grid; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.rank-list li { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 8px; + align-items: center; + padding: 9px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--soft); +} + +.anomaly-list li { + grid-template-columns: auto 1fr; + align-items: start; +} + +.anomaly-list li strong { + grid-column: 2; + color: var(--muted); + font-size: 12px; + line-height: 1.35; +} + .dashboard-band { margin-top: 12px; padding: 14px; @@ -530,6 +672,12 @@ h1 { cursor: pointer; } +.small-button.primary { + border-color: #1d4ed8; + background: #2563eb; + color: #ffffff; +} + a.small-button { display: inline-flex; align-items: center; @@ -569,7 +717,17 @@ pre { font-size: 12px; } +@media (max-width: 1280px) { + .executive-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .analytics-grid { grid-template-columns: 1fr; } +} + @media (max-width: 640px) { + .app-layout { grid-template-columns: 1fr; } + .sidebar { + position: static; + min-height: auto; + } .shell { padding: 14px; } .topbar { flex-direction: column; } .topbar-side { justify-items: stretch; min-width: 0; width: 100%; } @@ -581,6 +739,8 @@ pre { .period-banner { flex-direction: column; align-items: flex-start; } .band-head { flex-direction: column; align-items: flex-start; } .row { grid-template-columns: 1fr; } + .executive-grid { grid-template-columns: 1fr; } + .analytics-grid { grid-template-columns: 1fr; } .incident-row { grid-template-columns: 1fr; } .evidence-row { grid-template-columns: 1fr; } .readiness-card { grid-column: span 1; } diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index b9b8903..1b40b76 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -182,8 +182,10 @@ function periodConfig(period = state.period) { function periodFromSelectValue(value) { const text = String(value || "").toLowerCase(); - if (text.includes("нед")) return "week"; - if (text.includes("мес")) return "month"; + if (text === "week") return "week"; + if (text === "month") return "month"; + if (text.includes("нед") || text.includes("7")) return "week"; + if (text.includes("мес") || text.includes("30")) return "month"; return "today"; } @@ -288,35 +290,113 @@ function renderDailyDetailNotice() { `; } -function metricCard(labelText, value, status, context) { +function metricCard(labelText, value, status, context, trend) { + const trendText = trend || context || ""; + const trendClass = statusClass(status).replace("status-", ""); return `
${escapeHtml(status || "INFO")} -

${ui(labelText)}

-

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

-

${ui(context || "")}

+ ${ui(labelText)} + ${ui(value ?? "Нет данных")} + ${ui(trendText)}
`; } function renderExecutiveMetrics(report, incidents) { const activity = findKpi(report, "Индекс активности"); + const activeTime = findKpi(report, "Активное время"); const employees = findKpi(report, "Сотрудники"); const risk = findKpi(report, "UEBA риск"); const open = findKpi(report, "Открытые вопросы"); - const departments = findKpi(report, "Подразделения"); - const evidence = findKpi(report, "Evidence"); const openCount = Array.isArray(incidents) ? incidents.filter(item => !item.acknowledged).length : 0; + const anomalies = Array.isArray(report?.workforce?.insights) ? report.workforce.insights.filter(item => item.status !== "OK").length : 0; return `
- ${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 || "оценка по правилам")} - ${metricCard("Новые инциденты", open?.value ?? openCount, open?.status || incidentStatusFromCount(openCount), "не взятые в работу")} - ${metricCard("Готовность отчета", periodReadinessText(report), report?.period_view?.status || (report?.operator_ok ? "OK" : report?.severity), "день / неделя / месяц")} - ${metricCard("Доказательная база", evidence?.value, evidence?.status, evidence?.context)} + ${metricCard("Активность", activity?.value, activity?.status, activity?.context || "к выбранному периоду")} + ${metricCard("Полезное время", activeTime?.value || "нет данных", activeTime?.status || "UNKNOWN", "на сотрудника")} + ${metricCard("Сотрудников онлайн", employees?.value, employees?.status, employees?.context || "в текущем срезе")} + ${metricCard("Риск-индекс", risk?.value, risk?.status, risk?.context || "низкий / средний / высокий")} + ${metricCard("Аномалии", anomalies, anomalies ? "WARN" : "OK", "требуют проверки")} + ${metricCard("Инциденты ИБ", open?.value ?? openCount, open?.status || incidentStatusFromCount(openCount), "за выбранный период")} +
+ `; +} + +function statusRiskLabel(status) { + const value = String(status || "INFO").toUpperCase(); + if (value === "FAIL") return "high"; + if (value === "WARN") return "medium"; + if (value === "OK") return "low"; + return "info"; +} + +function trendArrow(status) { + const value = String(status || "INFO").toUpperCase(); + if (value === "OK") return "→"; + if (value === "WARN") return "↓"; + if (value === "FAIL") return "↓"; + return "→"; +} + +function renderDepartmentTable(report) { + const items = (report?.workforce?.department_comparison || []).slice(0, 8); + if (!items.length) return `

Подразделения пока не рассчитаны.

`; + return ` + + + ${items.map(item => ` + + + + + + + `).join("")} +
ПодразделениеАктивностьТрендРиск
${ui(item.label)}${ui(item.value || "-")}${escapeHtml(trendArrow(item.status))}${ui(statusRiskLabel(item.status))}
+ `; +} + +function renderAnomalies(report) { + const insights = Array.isArray(report?.workforce?.insights) ? report.workforce.insights : []; + const items = insights.filter(item => item.status !== "OK").slice(0, 8); + if (!items.length) return `

Существенных аномалий за выбранный период нет.

`; + return ``; +} + +function renderTopRisks(report) { + const reasons = Array.isArray(report?.ueba_risk?.reasons) ? report.ueba_risk.reasons : []; + if (!reasons.length) return `

Существенных риск-сигналов нет.

`; + return `
    ${reasons.slice(0, 8).map((item, index) => ` +
  1. + ${index + 1} + ${ui(item.label || item.code || "Риск")} + +${escapeHtml(item.points || 0)} +
  2. + `).join("")}
`; +} + +function renderOverviewAnalytics(report) { + return ` +
+
+

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

+ ${renderDepartmentTable(report)} +
+
+

Аномалии

+ ${renderAnomalies(report)} +
+
+

Топ рисков

+ ${renderTopRisks(report)} +
`; } @@ -423,6 +503,7 @@ function renderOperator(data, report) { ${renderPeriodBanner(report)} ${renderExecutiveMetrics(report, incidents)} + ${renderOverviewAnalytics(report)}

Рабочая активность сотрудников

загрузка, простои, перегруз и дисциплина процессов
${renderSectionItems(workforce, "Срез по работе сотрудников пока не сформирован.")} @@ -925,6 +1006,36 @@ function renderReports(data) { `; } +function renderSettings() { + return ` +
+
+

Настройки

+

Параметры отображения портала и границы интерпретации данных.

+
+ только чтение +
+
+
+

Периоды

+
+
Сегоднятекущий дневной срезготово
+
7 днейсводка по накопленной дневной историизависит от истории
+
30 днеймесячная управленческая интерпретациязависит от истории
+
+
+
+

Модули продукта

+
+
Workforceактивность, рабочее время, подразделения, сотрудникиактивно
+
UEBAправила риска, аномалии, обычный профильv1
+
DLP-liteсобытия данных, материалы проверки, расследованияread-only
+
+
+
+ `; +} + async function refresh() { if (!state.links) state.links = await loadJson("/links"); const summary = await loadJson("/summary"); @@ -977,6 +1088,9 @@ async function refresh() { content.innerHTML = renderReports(data); updateFilters(data); } + if (state.tab === "settings") { + content.innerHTML = renderSettings(); + } } function setTab(tab) { @@ -1007,6 +1121,12 @@ document.getElementById("periodFilter")?.addEventListener("change", event => { refresh().catch(showError); }); +document.addEventListener("click", event => { + const button = event.target.closest("[data-open-reports]"); + if (!button) return; + setTab("reports"); +}); + document.addEventListener("click", event => { const button = event.target.closest("[data-incident-action]"); if (!button) return; diff --git a/adk-rust/crates/detmir-portal/src/static/index.html b/adk-rust/crates/detmir-portal/src/static/index.html index d171b02..f52f808 100644 --- a/adk-rust/crates/detmir-portal/src/static/index.html +++ b/adk-rust/crates/detmir-portal/src/static/index.html @@ -7,58 +7,62 @@ -
-
-
-

AWatch-rus

-

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

-

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

+
+
+ + - +
+
+
+

AWatch-rus

+

Пульс организации

+

Workforce, UEBA и события безопасности за выбранный период

+
+
+
+ + +
+
+ + + + JSON +
Загрузка
+
+
+
-
-
-
+
+
+
+ diff --git a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md index d4fbfe1..33e9caf 100644 --- a/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md +++ b/docs/PORTAL_WORKCABINET_UI_2026-06-04_RU.md @@ -13,16 +13,32 @@ ## Что изменено - Верхняя панель переименована в `AWatch-rus Активность / Риски / Расследования`. -- Добавлены элементы отчета: период, подразделение, ответственный, текстовый отчет/PDF, статус сбора данных. +- Интерфейс переведен в промышленную компоновку: + - темный левый sidebar; + - светлая аналитическая рабочая область; + - верхняя панель периода, подразделения и экспорта; + - compact B2B/SOC карточки и таблицы. +- Добавлены элементы отчета: период, подразделение, формирование отчета, экспорт Markdown/PDF/JSON, статус сбора данных. - Навигация приведена к продуктовой модели: - - `Пульс организации`; - - `Рабочее время`; - - `Подразделения`; + - `Обзор`; - `Сотрудники`; + - `Подразделения`; - `Риски`; - `Расследования`; - `Сетевой периметр`; - `Отчеты`. + - `Настройки`. +- Главный экран `Пульс организации` содержит 6 KPI-карточек: + - активность; + - полезное время; + - сотрудников онлайн; + - риск-индекс; + - аномалии; + - инциденты ИБ. +- Главный аналитический блок содержит 3 колонки: + - подразделения сегодня; + - аномалии; + - топ рисков. - Добавлены отдельные промышленные представления: - экран подразделений: рейтинг групп, ответственные, просадки и отклонения; - экран сотрудников: карточка сотрудника, активность, сессии, плановое время, последнее событие; @@ -86,7 +102,7 @@ cargo build --release --manifest-path adk-rust/Cargo.toml -p detmir-portal Playwright smoke: - заголовок страницы: `AWatch-rus Portal`; -- вкладки `Пульс организации`, `Рабочее время`, `Подразделения`, `Сотрудники`, `Риски`, `Расследования`, `Сетевой периметр`, `Отчеты` открываются; +- вкладки `Обзор`, `Сотрудники`, `Подразделения`, `Риски`, `Расследования`, `Сетевой периметр`, `Отчеты`, `Настройки` открываются; - JS-ошибок нет; - API-ответов с кодом 400+ нет; - раздел рисков включает темную тему.