diff --git a/adk-rust/crates/detmir-portal/src/static/app.js b/adk-rust/crates/detmir-portal/src/static/app.js index 8fcfdab..9bbf296 100644 --- a/adk-rust/crates/detmir-portal/src/static/app.js +++ b/adk-rust/crates/detmir-portal/src/static/app.js @@ -22,21 +22,21 @@ const VIEW_MODES = { title: "Роль руководителя", heading: "Представление руководителя", description: "Главный вывод, сводка руководителя, риски подразделений и карта рисков.", - stage: "Загрузка представления руководителя", + stage: "Формирование главного вывода", }, security: { label: "Безопасность", title: "Роль безопасности", heading: "Представление безопасности", description: "Очередь проверки, расследования, аудит решений и пакеты расследований.", - stage: "Загрузка представления безопасности", + stage: "Подготовка разделов проверки и расследований", }, operations: { label: "Эксплуатация", title: "Роль эксплуатации", heading: "Представление эксплуатации", description: "Полнота данных, качество сбора, ошибки и телеметрия рабочих мест.", - stage: "Загрузка представления эксплуатации", + stage: "Подготовка разделов эксплуатации", }, }; @@ -92,6 +92,7 @@ function displayText(value) { .replaceAll("Risk Narrative", "Главный вывод") .replaceAll("Risk Heatmap", "Карта рисков") .replaceAll("Business Risk", "Риски подразделений") + .replaceAll("Security Events", "События безопасности") .replaceAll("Security Correlation", "Связь рисков и активности") .replaceAll("Incident Candidates", "Требует проверки") .replaceAll("Incident Candidate", "Требует проверки") @@ -286,6 +287,17 @@ function setLoadStatus(status, stage, progress, options = {}) { renderLoadStatus(); } +function loadStatusLabel(status) { + return { + LOADING: "Загрузка данных", + READY: "Данные готовы", + EMPTY: "Данные отсутствуют", + STALE: "Данные устарели", + ERROR: "Ошибка получения данных", + UNKNOWN: "Состояние неизвестно", + }[status] || "Состояние неизвестно"; +} + function renderLoadStatus() { const box = document.getElementById("loadingStatus"); if (!box) return; @@ -297,7 +309,8 @@ function renderLoadStatus() { const updatedText = document.getElementById("loadingUpdatedText"); const bar = document.getElementById("loadingProgressBar"); box.className = `loading-status loading-status-${status.toLowerCase()}`; - if (stateText) stateText.textContent = status; + box.dataset.loadStatus = status; + if (stateText) stateText.textContent = loadStatusLabel(status); if (stageText) stageText.textContent = stage; if (updatedText) updatedText.textContent = `последнее обновление: ${formatLoadTime(state.load.lastUpdatedAt)}`; if (bar) bar.style.width = `${progress}%`; @@ -320,7 +333,7 @@ function renderLoadingContent(stage = "Данные загружаются") { return `
- Данные загружаются + Загрузка данных

${ui(stage)}

@@ -353,7 +366,7 @@ function renderLoadingContent(stage = "Данные загружаются") { function renderEmptyState(stage = "Данных за выбранный период нет") { return `
- EMPTY + Данные отсутствуют

Данных пока нет

${ui(stage)}

@@ -364,8 +377,8 @@ function renderErrorState(error) { const message = error?.message || error?.stack || String(error || "Неизвестная ошибка"); return `
- ERROR -

Ошибка загрузки данных

+ Ошибка получения данных +

Ошибка получения данных

Портал не получил актуальные данные. Подробности ниже.

${escapeHtml(message)}
@@ -376,7 +389,7 @@ function staleBanner(error) { const message = error?.message || String(error || "ошибка обновления"); return `
- STALE + Данные устарели Показаны ранее загруженные данные.

Последнее обновление: ${escapeHtml(formatLoadTime(state.load.lastUpdatedAt))}. Новая загрузка не завершилась: ${escapeHtml(message)}

@@ -2750,33 +2763,34 @@ function renderSettings(report) { async function refresh(options = {}) { const content = document.getElementById("content"); const background = Boolean(options.background); - const stage = options.stage || "Загрузка данных портала"; + const stage = options.stage || "Получение данных"; try { setLoadStatus("LOADING", stage, background ? 35 : 8); if (!background && content) content.innerHTML = renderLoadingContent(stage); if (!state.links) { - setLoadStatus("LOADING", "Загрузка ссылок портала", 18); + setLoadStatus("LOADING", "Получение данных", 18); state.links = await loadJson("/links"); } - setLoadStatus("LOADING", "Загрузка общей сводки", 32); + setLoadStatus("LOADING", "Расчёт показателей", 34); const summary = await loadJson("/summary"); - setLoadStatus("LOADING", "Проверка готовности системы", 44); + setLoadStatus("LOADING", "Расчёт показателей", 46); state.readiness = { bundle: await loadJson("/readiness/bundle").catch(error => ({ ok: false, error: error.message })), verify: state.readiness?.verify || null }; renderSummary(summary, state.readiness); - setLoadStatus("LOADING", tabLoadingStage(state.tab), 62); + setLoadStatus("LOADING", "Формирование главного вывода", 68); const tabResult = await loadCurrentTab(); + setLoadStatus("LOADING", "Подготовка разделов", 88); if (!hasTabData(state.tab, tabResult)) { - setLoadStatus("EMPTY", "Данных за выбранный период нет", 100); + setLoadStatus("EMPTY", "Данные отсутствуют", 100); if (content) { content.innerHTML = `${renderEmptyState("Источники ответили, но полезные записи для текущего раздела пока не найдены.")}${tabResult.html || ""}`; } return; } if (content) content.innerHTML = tabResult.html; - setLoadStatus("READY", "Данные загружены", 100); + setLoadStatus("READY", "Данные готовы", 100); consumePendingScroll(); } catch (error) { showError(error); @@ -2845,14 +2859,14 @@ async function loadCurrentTab() { function tabLoadingStage(tab) { if (tab === "operator") return currentViewMeta().stage; return { - employees: "Загрузка карточек сотрудников", - departments: "Загрузка подразделений", - owner: "Загрузка рисков", - incidents: "Загрузка расследований", - perimeter: "Загрузка сетевого периметра", - reports: "Формирование отчетов", - settings: "Загрузка параметров расчета", - }[tab] || "Загрузка раздела"; + employees: "Получение данных сотрудников", + departments: "Расчёт показателей подразделений", + owner: "Формирование главного вывода по рискам", + incidents: "Подготовка разделов расследований", + perimeter: "Подготовка разделов сетевого периметра", + reports: "Подготовка разделов отчета", + settings: "Подготовка раздела настроек", + }[tab] || "Подготовка разделов"; } function setTab(tab) { @@ -2907,7 +2921,7 @@ document.querySelectorAll("[data-view-mode]").forEach(btn => { document.getElementById("periodFilter")?.addEventListener("change", event => { state.period = periodFromSelectValue(event.target.value); - refresh({ stage: "Обновление периода отчета" }); + refresh({ stage: "Получение данных за выбранный период" }); }); document.addEventListener("click", event => { diff --git a/adk-rust/crates/detmir-portal/src/static/index.html b/adk-rust/crates/detmir-portal/src/static/index.html index 758989b..5150901 100644 --- a/adk-rust/crates/detmir-portal/src/static/index.html +++ b/adk-rust/crates/detmir-portal/src/static/index.html @@ -57,10 +57,10 @@
- - - JSON -
Загрузка
+ + + Данные +
Загрузка данных
@@ -68,8 +68,8 @@
- LOADING - Инициализация портала + Загрузка данных + Получение данных
последнее обновление: -
diff --git a/docs/PORTAL_RU.md b/docs/PORTAL_RU.md index a2ee0e6..f667ff9 100644 --- a/docs/PORTAL_RU.md +++ b/docs/PORTAL_RU.md @@ -53,21 +53,29 @@ SIEM-представление не выводятся. данных`. Она показывает, что сейчас происходит с данными, и не смешивает состояния `нет данных` и `данные еще загружаются`. -Статусы: +Видимые статусы: -- `LOADING` - портал загружает сводку, отчеты или выбранный раздел; -- `READY` - данные успешно загружены и экран актуален; -- `EMPTY` - источники ответили, но полезных записей для выбранного раздела - или периода нет; -- `STALE` - показаны ранее загруженные данные, новое фоновое обновление не - завершилось; -- `ERROR` - первичная загрузка не удалась, актуальных данных на экране нет. +- `Загрузка данных` - портал получает данные и готовит экран; +- `Данные готовы` - данные успешно загружены и экран актуален; +- `Данные отсутствуют` - источники ответили, но полезных записей для выбранного + раздела или периода нет; +- `Данные устарели` - показаны ранее загруженные данные, новое фоновое + обновление не завершилось; +- `Ошибка получения данных` - первичная загрузка не удалась, актуальных данных + на экране нет. -Во время `LOADING` портал показывает заготовки карточек и текст `Данные -загружаются`; пустые таблицы и сообщения `Нет данных` не выводятся до -завершения загрузки. После успешного обновления отображается время последнего -обновления. При `STALE` старый экран остается доступным, но сверху появляется -предупреждение о неудачном обновлении. +Этапы прогресса: + +- `Получение данных`; +- `Расчёт показателей`; +- `Формирование главного вывода`; +- `Подготовка разделов`. + +Во время загрузки портал показывает заготовки карточек и текст `Загрузка +данных`; пустые таблицы и сообщения `Нет данных` не выводятся до завершения +загрузки. После успешного обновления отображается время последнего обновления. +При состоянии `Данные устарели` старый экран остается доступным, но сверху +появляется предупреждение о неудачном обновлении. ## Сводка руководителя diff --git a/scripts/detmir-portal-tabs-smoke.mjs b/scripts/detmir-portal-tabs-smoke.mjs index a93ec77..08e0b0d 100644 --- a/scripts/detmir-portal-tabs-smoke.mjs +++ b/scripts/detmir-portal-tabs-smoke.mjs @@ -69,6 +69,21 @@ function assertStaticTabHandlers() { if (missing.length) { throw new Error(`data-tab without app.js handler: ${missing.join(", ")}`); } + const loadingTerms = [ + "Загрузка данных", + "Данные готовы", + "Данные отсутствуют", + "Данные устарели", + "Ошибка получения данных", + "Получение данных", + "Расчёт показателей", + "Формирование главного вывода", + "Подготовка разделов", + ]; + const missingLoadingTerms = loadingTerms.filter((term) => !app.includes(term) && !index.includes(term)); + if (missingLoadingTerms.length) { + throw new Error(`missing loading/localization terms: ${missingLoadingTerms.join(", ")}`); + } return tabs; } @@ -124,7 +139,7 @@ async function main() { await page.waitForSelector(".tab", { timeout }); smokeStep = "wait_initial_ready"; await page.waitForFunction( - () => document.querySelector("#loadingStateText")?.textContent === "READY", + () => document.querySelector("#loadingStatus")?.dataset.loadStatus === "READY", null, { timeout }, ); @@ -132,8 +147,9 @@ async function main() { name: "loading_refresh_status_ready", ok: (await page.locator("#loadingStatus").count()) === 1 - && (await page.locator("#loadingStageText").innerText({ timeout })).includes("Данные загружены") - && !(await page.locator("body").innerText({ timeout })).includes("Данные загружаются"), + && (await page.locator("#loadingStateText").innerText({ timeout })).includes("Данные готовы") + && (await page.locator("#loadingStageText").innerText({ timeout })).includes("Данные готовы") + && !(await page.locator("body").innerText({ timeout })).includes("Загрузка данных"), }); smokeStep = "api:security_events_summary"; const reportsPayload = await page.evaluate(async () => { @@ -230,7 +246,7 @@ async function main() { smokeStep = "role:security"; await page.click('[data-view-mode="security"]', { timeout }); await page.waitForFunction( - () => document.querySelector("#loadingStateText")?.textContent === "READY" + () => document.querySelector("#loadingStatus")?.dataset.loadStatus === "READY" && document.body.innerText.includes("Материалы расследования"), null, { timeout }, @@ -258,7 +274,7 @@ async function main() { smokeStep = "role:operations"; await page.click('[data-view-mode="operations"]', { timeout }); await page.waitForFunction( - () => document.querySelector("#loadingStateText")?.textContent === "READY" + () => document.querySelector("#loadingStatus")?.dataset.loadStatus === "READY" && document.body.innerText.includes("Телеметрия"), null, { timeout }, @@ -306,7 +322,7 @@ async function main() { await page.click('button[data-tab="operator"]', { timeout }); await page.click('[data-view-mode="security"]', { timeout }); await page.waitForFunction( - () => document.querySelector("#loadingStateText")?.textContent === "READY" + () => document.querySelector("#loadingStatus")?.dataset.loadStatus === "READY" && document.body.innerText.includes("Требует проверки"), null, { timeout },