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 @@
@@ -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 },