feat(portal): add role views
This commit is contained in:
@@ -142,13 +142,58 @@ h1 {
|
||||
}
|
||||
|
||||
.control-strip,
|
||||
.header-actions {
|
||||
.header-actions,
|
||||
.view-switch {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.view-switch {
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.view-switch > span {
|
||||
padding: 0 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.view-button {
|
||||
min-height: 30px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 5px 9px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-button.is-active {
|
||||
border-color: #1d4ed8;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.view-button:hover {
|
||||
color: var(--ink);
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.view-button.is-active:hover {
|
||||
color: #ffffff;
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.control-strip label {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
@@ -1114,8 +1159,11 @@ pre {
|
||||
.topbar { flex-direction: column; }
|
||||
.topbar-side { justify-items: stretch; min-width: 0; width: 100%; }
|
||||
.control-strip,
|
||||
.header-actions { justify-content: flex-start; }
|
||||
.header-actions,
|
||||
.view-switch { justify-content: flex-start; }
|
||||
.control-strip label { min-width: 100%; }
|
||||
.view-switch > span { flex-basis: 100%; }
|
||||
.view-button { flex: 1 1 120px; }
|
||||
h1 { font-size: 24px; }
|
||||
.page-head { flex-direction: column; }
|
||||
.period-banner { flex-direction: column; align-items: flex-start; }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const state = {
|
||||
tab: "operator",
|
||||
viewMode: initialViewMode(),
|
||||
period: "today",
|
||||
links: null,
|
||||
readiness: null,
|
||||
reports: null,
|
||||
cases: null,
|
||||
pendingScrollSelector: null,
|
||||
load: {
|
||||
status: "LOADING",
|
||||
@@ -14,6 +16,39 @@ const state = {
|
||||
},
|
||||
};
|
||||
|
||||
const VIEW_MODES = {
|
||||
executive: {
|
||||
label: "Руководитель",
|
||||
title: "Роль руководителя",
|
||||
heading: "Представление руководителя",
|
||||
description: "Главный вывод, сводка руководителя, риски подразделений и карта рисков.",
|
||||
stage: "Загрузка представления руководителя",
|
||||
},
|
||||
security: {
|
||||
label: "Безопасность",
|
||||
title: "Роль безопасности",
|
||||
heading: "Представление безопасности",
|
||||
description: "Очередь проверки, расследования, аудит решений и пакеты расследований.",
|
||||
stage: "Загрузка представления безопасности",
|
||||
},
|
||||
operations: {
|
||||
label: "Эксплуатация",
|
||||
title: "Роль эксплуатации",
|
||||
heading: "Представление эксплуатации",
|
||||
description: "Полнота данных, качество сбора, ошибки и телеметрия рабочих мест.",
|
||||
stage: "Загрузка представления эксплуатации",
|
||||
},
|
||||
};
|
||||
|
||||
function initialViewMode() {
|
||||
try {
|
||||
const stored = window.localStorage?.getItem("detmir.portal.viewMode");
|
||||
return ["executive", "security", "operations"].includes(stored) ? stored : "executive";
|
||||
} catch {
|
||||
return "executive";
|
||||
}
|
||||
}
|
||||
|
||||
function apiBase() {
|
||||
const path = window.location.pathname;
|
||||
return path.startsWith("/portal") ? "/portal/api" : "/api";
|
||||
@@ -142,6 +177,36 @@ function tooltip(value) {
|
||||
return `title="${ui(value)}"`;
|
||||
}
|
||||
|
||||
function currentViewMode() {
|
||||
return VIEW_MODES[state.viewMode] ? state.viewMode : "executive";
|
||||
}
|
||||
|
||||
function currentViewMeta() {
|
||||
return VIEW_MODES[currentViewMode()];
|
||||
}
|
||||
|
||||
function updateViewModeButtons() {
|
||||
document.querySelectorAll("[data-view-mode]").forEach(button => {
|
||||
button.classList.toggle("is-active", button.dataset.viewMode === currentViewMode());
|
||||
});
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
if (!VIEW_MODES[mode]) return;
|
||||
state.viewMode = mode;
|
||||
try {
|
||||
window.localStorage?.setItem("detmir.portal.viewMode", mode);
|
||||
} catch {
|
||||
// View mode is a UI preference only; ignore storage failures.
|
||||
}
|
||||
updateViewModeButtons();
|
||||
if (state.tab !== "operator") {
|
||||
setTab("operator");
|
||||
return;
|
||||
}
|
||||
refresh({ stage: VIEW_MODES[mode].stage });
|
||||
}
|
||||
|
||||
function renderSummary(summary, readiness) {
|
||||
const global = document.getElementById("globalStatus");
|
||||
global.className = `status-pill ${statusClass(summary.severity)}`;
|
||||
@@ -945,7 +1010,7 @@ function renderDepartmentLeaderCard(report) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOverviewAnalytics(report) {
|
||||
function renderOperatorDetailBands(report) {
|
||||
return `
|
||||
${renderDepartmentRanking(report)}
|
||||
${renderAttentionBlock(report)}
|
||||
@@ -1053,35 +1118,189 @@ function renderSourceList(data) {
|
||||
`).join("")}</div>`;
|
||||
}
|
||||
|
||||
function renderOperator(data, report) {
|
||||
function renderOperator(data, report, extras = {}) {
|
||||
report = periodReport(report);
|
||||
const incidents = Array.isArray(data.incidents) ? data.incidents : [];
|
||||
const workforce = findSection(report, "Работа");
|
||||
const insights = findSection(report, "Выводы Workforce");
|
||||
const security = findSection(report, "ИБ");
|
||||
const actions = findSection(report, "Действия");
|
||||
const meta = currentViewMeta();
|
||||
return `
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2 class="section-title">Управленческая сводка</h2>
|
||||
<p class="muted">За 10 секунд: работают ли люди, где просадка, где риск и что требует внимания.</p>
|
||||
<h2 class="section-title">${ui(meta.heading)}</h2>
|
||||
<p class="muted">${ui(meta.description)}</p>
|
||||
</div>
|
||||
<span class="badge ${statusClass(report?.severity)}">${escapeHtml(report?.headline || "Оперативный срез")}</span>
|
||||
<span class="badge ${statusClass(report?.severity)}">${ui(meta.label)}</span>
|
||||
</div>
|
||||
${renderPeriodBanner(report)}
|
||||
${renderRoleViewSummary(meta)}
|
||||
${renderOperatorRoleContent(currentViewMode(), data, report, { ...extras, incidents })}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRoleViewSummary(meta) {
|
||||
return `
|
||||
<section class="card role-view-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>${ui(meta.heading)}</h3>
|
||||
<p class="muted">${ui(meta.description)}</p>
|
||||
</div>
|
||||
<span class="badge status-ok">${ui(meta.label)}</span>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOperatorRoleContent(mode, data, report, extras = {}) {
|
||||
if (mode === "security") return renderSecurityView(data, report, extras);
|
||||
if (mode === "operations") return renderOperationsView(data, report);
|
||||
return renderExecutiveView(report, extras.incidents || []);
|
||||
}
|
||||
|
||||
function renderExecutiveView(report, incidents) {
|
||||
return `
|
||||
${renderRiskNarrative(report)}
|
||||
${renderExecutiveDashboard(report)}
|
||||
${renderExecutiveMetrics(report, incidents)}
|
||||
${renderAgentQuality(report?.agent_quality, report?.agent_quality_explain)}
|
||||
${renderAgentQualityHistory(report?.agent_quality_history, report?.agent_quality_history_summary)}
|
||||
${renderAgentQualityNodes(report?.agent_quality_nodes, report?.agent_quality_nodes_summary)}
|
||||
${renderAgentCoverageSla(report?.agent_coverage_sla)}
|
||||
${renderRiskHeatmap(report?.risk_heatmap)}
|
||||
${renderSecurityCorrelation(report?.security_correlation)}
|
||||
${renderBusinessRisk(report?.business_risk)}
|
||||
${renderBusinessRiskTimeline(report?.business_risk_history, report?.business_risk_history_summary)}
|
||||
${renderRiskIncidentCandidates(report?.risk_incident_candidates)}
|
||||
${renderRiskHeatmap(report?.risk_heatmap)}
|
||||
${renderOverviewAnalytics(report)}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSecurityView(data, report, extras = {}) {
|
||||
const cases = Array.isArray(extras.cases?.cases) ? extras.cases.cases : [];
|
||||
return `
|
||||
${renderRiskIncidentCandidates(report?.risk_incident_candidates)}
|
||||
${renderCases(cases)}
|
||||
${renderIncidentReviewAuditSummary(report)}
|
||||
${renderInvestigationPacks(report?.risk_incident_candidates)}
|
||||
<section class="dashboard-band security-band">
|
||||
<div class="band-head"><h3>Расследования</h3><span class="muted">ручная проверка, решения и материалы</span></div>
|
||||
${renderIncidentsList(extras.incidents || data.incidents || [])}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOperationsView(data, report) {
|
||||
return `
|
||||
${renderAgentCoverageSla(report?.agent_coverage_sla)}
|
||||
${renderAgentQuality(report?.agent_quality, report?.agent_quality_explain)}
|
||||
${renderOperationsErrors(data, report)}
|
||||
<section class="dashboard-band technical-band">
|
||||
<div class="band-head"><h3>Телеметрия</h3><span class="muted">свежесть, стабильность и источники данных</span></div>
|
||||
${renderAgentQualityHistory(report?.agent_quality_history, report?.agent_quality_history_summary)}
|
||||
${renderAgentQualityNodes(report?.agent_quality_nodes, report?.agent_quality_nodes_summary)}
|
||||
${renderSourceList(data)}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOperationsErrors(data, report) {
|
||||
const rows = [];
|
||||
const collectorError = report?.agent_quality?.collector_error;
|
||||
if (collectorError) {
|
||||
rows.push(["Ошибка коллектора", collectorError, report?.agent_quality?.quality_status || "DEGRADED"]);
|
||||
}
|
||||
const problemNodes = Array.isArray(report?.agent_coverage_sla?.problem_nodes)
|
||||
? report.agent_coverage_sla.problem_nodes
|
||||
: [];
|
||||
for (const node of problemNodes.slice(0, 8)) {
|
||||
rows.push([
|
||||
node.hostname || "unknown",
|
||||
`${node.status || "UNKNOWN"} · ${node.recommendation || "Проверить рабочее место"}`,
|
||||
node.status || "WARNING",
|
||||
]);
|
||||
}
|
||||
for (const [name, source] of Object.entries(data || {})) {
|
||||
if (!source || typeof source !== "object") continue;
|
||||
const status = source.status || (source.ok === false ? "FAIL" : "");
|
||||
if (!status || ["OK", "READY", "INFO"].includes(String(status).toUpperCase())) continue;
|
||||
rows.push([label(name), source.summary || source.error || "требуется проверка", status]);
|
||||
}
|
||||
return `
|
||||
<section class="card operations-errors-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>Ошибки</h3>
|
||||
<p class="muted">Что мешает достоверному сбору и эксплуатационной готовности.</p>
|
||||
</div>
|
||||
<span class="badge ${statusClass(rows.length ? "WARNING" : "OK")}">${rows.length}</span>
|
||||
</div>
|
||||
<div class="list compact-list">${rows.length ? rows.map(([name, text, status]) => `
|
||||
<div class="row compact-row">
|
||||
<strong>${ui(name)}</strong>
|
||||
<span class="muted">${ui(text)}</span>
|
||||
<span class="badge ${statusClass(status)}">${ui(status)}</span>
|
||||
</div>
|
||||
`).join("") : `
|
||||
<div class="row compact-row">
|
||||
<strong>Критичных ошибок нет</strong>
|
||||
<span class="muted">Портал не видит ошибок коллектора или проблемных рабочих мест в текущем срезе.</span>
|
||||
<span class="badge status-ok">OK</span>
|
||||
</div>
|
||||
`}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderIncidentReviewAuditSummary(report) {
|
||||
const summary = report?.incident_review_audit_summary || {};
|
||||
const rows = [
|
||||
["Всего изменений", summary.total_changes ?? 0],
|
||||
["Подтверждено", summary.confirmed_count ?? 0],
|
||||
["Ложные срабатывания", summary.false_positive_count ?? 0],
|
||||
["Отложено", summary.postponed_count ?? 0],
|
||||
["Последнее изменение", summary.last_change_utc || "-"],
|
||||
];
|
||||
return `
|
||||
<section class="card incident-audit-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>Аудит</h3>
|
||||
<p class="muted">Кто и когда менял статус записей, требующих проверки.</p>
|
||||
</div>
|
||||
<span class="badge ${statusClass(Number(summary.total_changes || 0) > 0 ? "INFO" : "UNKNOWN")}">${ui(summary.total_changes ?? 0)}</span>
|
||||
</div>
|
||||
<div class="quality-grid">${rows.map(([name, value]) => `
|
||||
<div><span class="muted">${ui(name)}</span><strong>${ui(value)}</strong></div>
|
||||
`).join("")}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderInvestigationPacks(candidates) {
|
||||
const rows = Array.isArray(candidates) ? candidates.slice(0, 10) : [];
|
||||
return `
|
||||
<section class="card investigation-packs-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h3>Пакеты расследований</h3>
|
||||
<p class="muted">Выгружаемые материалы по каждой записи для ручной проверки.</p>
|
||||
</div>
|
||||
<span class="badge ${statusClass(rows.length ? "INFO" : "UNKNOWN")}">${rows.length}</span>
|
||||
</div>
|
||||
<div class="list compact-list">${rows.length ? rows.map(item => `
|
||||
<div class="row compact-row">
|
||||
<strong>${ui(item.department || "Без подразделения")}</strong>
|
||||
<span class="muted">${ui(item.reason || "требуется проверка")} · ${ui(item.hostname || "-")}</span>
|
||||
<a class="small-button" href="${apiBase()}/investigation-pack/${encodeURIComponent(item.id || "")}?format=markdown" download>Скачать</a>
|
||||
</div>
|
||||
`).join("") : `
|
||||
<div class="row compact-row">
|
||||
<strong>Пакетов нет</strong>
|
||||
<span class="muted">Нет записей, требующих выгрузки материалов.</span>
|
||||
<span class="badge status-ok">OK</span>
|
||||
</div>
|
||||
`}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderOverviewAnalytics(report) {
|
||||
const workforce = findSection(report, "Работа");
|
||||
const insights = findSection(report, "Выводы по активности") || findSection(report, "Выводы Workforce");
|
||||
const security = findSection(report, "ИБ");
|
||||
const actions = findSection(report, "Действия");
|
||||
return `
|
||||
<section class="dashboard-band">
|
||||
<div class="band-head"><h3>Рабочая активность сотрудников</h3><span class="muted">загрузка, простои, перегруз и дисциплина процессов</span></div>
|
||||
${renderSectionItems(workforce, "Срез по работе сотрудников пока не сформирован.")}
|
||||
@@ -1093,13 +1312,8 @@ function renderOperator(data, report) {
|
||||
</section>
|
||||
<section class="dashboard-band">
|
||||
<div class="band-head"><h3>Что требует внимания</h3><span class="muted">операторские действия и открытые вопросы</span></div>
|
||||
${renderIncidentsList(incidents)}
|
||||
${renderSectionItems(actions, "Рекомендаций нет.")}
|
||||
</section>
|
||||
<section class="dashboard-band technical-band">
|
||||
<div class="band-head"><h3>Технический статус источников</h3><span class="muted">вторичный слой для оператора</span></div>
|
||||
${renderSourceList(data)}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2457,8 +2671,11 @@ async function loadCurrentTab() {
|
||||
if (state.tab === "operator") {
|
||||
const data = await loadJson("/operator");
|
||||
state.reports = await loadJson("/reports").catch(() => state.reports);
|
||||
if (currentViewMode() === "security") {
|
||||
state.cases = await loadJson("/cases").catch(error => ({ ok: false, error: error.message, cases: [] }));
|
||||
}
|
||||
updateFilters(state.reports);
|
||||
return { data, report: state.reports, html: renderOperator(data, state.reports) };
|
||||
return { data, report: state.reports, cases: state.cases, html: renderOperator(data, state.reports, { cases: state.cases }) };
|
||||
}
|
||||
if (state.tab === "manager") {
|
||||
const data = await loadJson("/manager");
|
||||
@@ -2510,8 +2727,8 @@ async function loadCurrentTab() {
|
||||
}
|
||||
|
||||
function tabLoadingStage(tab) {
|
||||
if (tab === "operator") return currentViewMeta().stage;
|
||||
return {
|
||||
operator: "Загрузка связанной картины риска",
|
||||
employees: "Загрузка карточек сотрудников",
|
||||
departments: "Загрузка подразделений",
|
||||
owner: "Загрузка рисков",
|
||||
@@ -2568,6 +2785,10 @@ document.querySelectorAll(".tab").forEach(btn => {
|
||||
btn.addEventListener("click", () => setTab(btn.dataset.tab));
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-view-mode]").forEach(btn => {
|
||||
btn.addEventListener("click", () => setViewMode(btn.dataset.viewMode));
|
||||
});
|
||||
|
||||
document.getElementById("periodFilter")?.addEventListener("change", event => {
|
||||
state.period = periodFromSelectValue(event.target.value);
|
||||
refresh({ stage: "Обновление периода отчета" });
|
||||
@@ -2768,6 +2989,7 @@ async function caseStatusAction(button) {
|
||||
await refresh({ stage: "Обновление статуса дела" });
|
||||
}
|
||||
|
||||
updateViewModeButtons();
|
||||
applySecurityMode(state.tab);
|
||||
renderLoadStatus();
|
||||
refresh({ stage: "Первичная загрузка портала" });
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="view-switch" aria-label="Ролевое представление">
|
||||
<span>Представление</span>
|
||||
<button class="view-button is-active" data-view-mode="executive" type="button">Руководитель</button>
|
||||
<button class="view-button" data-view-mode="security" type="button">Безопасность</button>
|
||||
<button class="view-button" data-view-mode="operations" type="button">Эксплуатация</button>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="small-button primary" data-open-reports="true">Сформировать отчет</button>
|
||||
<button class="small-button" data-export-markdown="true">Markdown</button>
|
||||
|
||||
@@ -19,6 +19,21 @@
|
||||
- `Отчеты` - текстовый отчет, печать/PDF, выгрузка данных и управленческий текст.
|
||||
- `Настройки` - параметры расчета только для чтения.
|
||||
|
||||
## Ролевые представления
|
||||
|
||||
Во вкладке `Обзор` есть переключатель представления:
|
||||
|
||||
- `Руководитель` - главный вывод, сводка руководителя, риски подразделений и
|
||||
карта рисков;
|
||||
- `Безопасность` - записи, которые требуют проверки, расследования, аудит
|
||||
решений и пакеты расследований;
|
||||
- `Эксплуатация` - полнота данных, качество сбора, ошибки и телеметрия рабочих
|
||||
мест.
|
||||
|
||||
Переключатель не меняет модель данных и не создает новые API. Он только
|
||||
группирует существующие блоки портала под разные категории пользователей:
|
||||
директор, сотрудник безопасности и администратор эксплуатации.
|
||||
|
||||
## Загрузка и актуальность данных
|
||||
|
||||
В верхней части портала есть глобальная полоса `Загрузка и актуальность
|
||||
|
||||
@@ -59,7 +59,7 @@ function assertStaticTabHandlers() {
|
||||
}
|
||||
|
||||
const expectedTabs = [
|
||||
{ tab: "operator", label: "Обзор", marker: "Управленческая сводка" },
|
||||
{ tab: "operator", label: "Обзор", marker: "Представление руководителя" },
|
||||
{ tab: "employees", label: "Сотрудники", marker: "Карточки сотрудников" },
|
||||
{ tab: "departments", label: "Подразделения", marker: "Рейтинг подразделений" },
|
||||
{ tab: "owner", label: "Риски", marker: "Контроль безопасности" },
|
||||
@@ -69,6 +69,8 @@ const expectedTabs = [
|
||||
{ tab: "settings", label: "Настройки", marker: "Параметры расчета" },
|
||||
];
|
||||
|
||||
let smokeStep = "start";
|
||||
|
||||
async function main() {
|
||||
const staticTabs = assertStaticTabHandlers();
|
||||
const missingExpected = expectedTabs.filter((item) => !staticTabs.includes(item.tab));
|
||||
@@ -102,8 +104,11 @@ async function main() {
|
||||
const results = [];
|
||||
const checks = [];
|
||||
try {
|
||||
smokeStep = "open_portal";
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
||||
smokeStep = "wait_tabs";
|
||||
await page.waitForSelector(".tab", { timeout });
|
||||
smokeStep = "wait_initial_ready";
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#loadingStateText")?.textContent === "READY",
|
||||
null,
|
||||
@@ -117,6 +122,7 @@ async function main() {
|
||||
&& !(await page.locator("body").innerText({ timeout })).includes("Данные загружаются"),
|
||||
});
|
||||
for (const item of expectedTabs) {
|
||||
smokeStep = `tab:${item.tab}`;
|
||||
await page.click(`button[data-tab="${item.tab}"]`, { timeout });
|
||||
await page.waitForFunction(
|
||||
(marker) => document.body && document.body.innerText.includes(marker),
|
||||
@@ -129,32 +135,18 @@ async function main() {
|
||||
const ok = activeTab.trim() === item.label && bodyText.includes(item.marker) && !stillLoading;
|
||||
results.push({ tab: item.tab, label: item.label, marker: item.marker, ok });
|
||||
if (item.tab === "operator") {
|
||||
await page.waitForFunction(
|
||||
() => document.body && document.body.innerText.includes("Качество данных по рабочим местам"),
|
||||
null,
|
||||
{ timeout },
|
||||
);
|
||||
const readyBodyText = await page.locator("#content").innerText({ timeout });
|
||||
const requiredExecutive = [
|
||||
"Главный вывод",
|
||||
"Сотрудников в работе",
|
||||
"Средний индекс активности",
|
||||
"Достоверность показателей",
|
||||
"Полнота данных",
|
||||
"Главная причина риска",
|
||||
"Подтверждающие слои",
|
||||
"Качество данных",
|
||||
"Стабильность агента за 7 дней",
|
||||
"Качество данных по рабочим местам",
|
||||
"Полнота данных",
|
||||
"Карта рисков",
|
||||
"Связь рисков и активности",
|
||||
"Риски подразделений",
|
||||
"Динамика рисков",
|
||||
"Требует проверки",
|
||||
"ТОП-5 лучших подразделений",
|
||||
"ТОП-5 проблемных подразделений",
|
||||
"Требует внимания",
|
||||
"Heat Map подразделений",
|
||||
"Карточка руководителя подразделения",
|
||||
"Рабочая активность сотрудников",
|
||||
"Контроль безопасности",
|
||||
"Что требует внимания",
|
||||
];
|
||||
checks.push({
|
||||
name: "executive_dashboard_layer",
|
||||
@@ -166,25 +158,70 @@ async function main() {
|
||||
);
|
||||
const riskNarrativeIndex = cardHeadings.indexOf("Главный вывод");
|
||||
const executiveIndex = cardHeadings.indexOf("Сводка руководителя");
|
||||
const trustIndex = cardHeadings.indexOf("Качество данных");
|
||||
const businessRiskIndex = cardHeadings.indexOf("Риски подразделений");
|
||||
const candidatesIndex = cardHeadings.indexOf("Требует проверки");
|
||||
const heatmapIndex = cardHeadings.indexOf("Карта рисков");
|
||||
checks.push({
|
||||
name: "management_block_order",
|
||||
ok:
|
||||
riskNarrativeIndex >= 0 &&
|
||||
executiveIndex > riskNarrativeIndex &&
|
||||
trustIndex > executiveIndex &&
|
||||
businessRiskIndex > trustIndex &&
|
||||
candidatesIndex > businessRiskIndex,
|
||||
businessRiskIndex > executiveIndex &&
|
||||
heatmapIndex > businessRiskIndex,
|
||||
order: [
|
||||
"Главный вывод",
|
||||
"Сводка руководителя",
|
||||
"Качество данных",
|
||||
"Риски подразделений",
|
||||
"Требует проверки",
|
||||
"Карта рисков",
|
||||
],
|
||||
});
|
||||
checks.push({
|
||||
name: "role_view_switcher_present",
|
||||
ok:
|
||||
(await page.locator('[data-view-mode="executive"]').count()) === 1
|
||||
&& (await page.locator('[data-view-mode="security"]').count()) === 1
|
||||
&& (await page.locator('[data-view-mode="operations"]').count()) === 1,
|
||||
});
|
||||
|
||||
smokeStep = "role:security";
|
||||
await page.click('[data-view-mode="security"]', { timeout });
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#loadingStateText")?.textContent === "READY"
|
||||
&& document.body.innerText.includes("Пакеты расследований"),
|
||||
null,
|
||||
{ timeout },
|
||||
);
|
||||
const securityText = await page.locator("#content").innerText({ timeout });
|
||||
checks.push({
|
||||
name: "security_role_view",
|
||||
ok: [
|
||||
"Представление безопасности",
|
||||
"Требует проверки",
|
||||
"Расследования",
|
||||
"Аудит",
|
||||
"Пакеты расследований",
|
||||
].every((marker) => containsText(securityText, marker)),
|
||||
});
|
||||
|
||||
smokeStep = "role:operations";
|
||||
await page.click('[data-view-mode="operations"]', { timeout });
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#loadingStateText")?.textContent === "READY"
|
||||
&& document.body.innerText.includes("Телеметрия"),
|
||||
null,
|
||||
{ timeout },
|
||||
);
|
||||
const operationsText = await page.locator("#content").innerText({ timeout });
|
||||
checks.push({
|
||||
name: "operations_role_view",
|
||||
ok: [
|
||||
"Представление эксплуатации",
|
||||
"Полнота данных",
|
||||
"Качество данных",
|
||||
"Качество данных по рабочим местам",
|
||||
"Ошибки",
|
||||
"Телеметрия",
|
||||
].every((marker) => containsText(operationsText, marker)),
|
||||
});
|
||||
}
|
||||
if (item.tab === "settings") {
|
||||
const requiredSettings = [
|
||||
@@ -202,9 +239,12 @@ async function main() {
|
||||
});
|
||||
}
|
||||
}
|
||||
smokeStep = "security:investigation_navigation";
|
||||
await page.click('button[data-tab="operator"]', { timeout });
|
||||
await page.click('[data-view-mode="security"]', { timeout });
|
||||
await page.waitForFunction(
|
||||
() => document.body && document.body.innerText.includes("Топ рисков"),
|
||||
() => document.querySelector("#loadingStateText")?.textContent === "READY"
|
||||
&& document.body.innerText.includes("Требует проверки"),
|
||||
null,
|
||||
{ timeout },
|
||||
);
|
||||
@@ -255,6 +295,6 @@ async function main() {
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stdout.write(`${JSON.stringify({ ok: false, error: error.message }, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify({ ok: false, step: smokeStep, error: error.message, stack: error.stack }, null, 2)}\n`);
|
||||
process.exitCode = 2;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user