diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index 2eb4572d..d2dc398b 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -870,6 +870,34 @@ function SortIcon({ k, sortKey, sortAsc }: { k: ResSortKey; sortKey: ResSortKey; type ResTypeFilter = "all" | "jump-host" | "exit-node" | "home-router" +function ResourceWarnings({ rows }: { rows: UptimeResourceRow[] }) { + const [expanded, setExpanded] = useState(false) + const [visible, setVisible] = useState(55) + if (!rows.length) return null + function renderRow(row: UptimeResourceRow) { + const issues: string[] = [] + if (row.cpu >= 85) issues.push(`CPU ${row.cpu}%`) + if (row.ramPct >= 85) issues.push(`RAM ${row.ramPct}%`) + if (row.hddPct >= 85) issues.push(`HDD ${row.hddPct}%`) + if ((row.temp ?? 0) >= 70) issues.push(`${row.temp}°C`) + return + {row.server.name} — {issues.join(", ")} + + } + return + + Серверов с предупреждениями: {rows.length} + +
{rows.slice(0, 5).map(renderRow)}
+ {rows.length > 5 &&
{ setExpanded(event.currentTarget.open); if (!event.currentTarget.open) setVisible(55) }}> + Ещё {rows.length - 5} + {expanded &&
{rows.slice(5, visible).map(renderRow)}
} + {expanded && rows.length > visible && } +
} +
+
+} + function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerResource[]; serversList: Server[]; liveApi?: boolean }) { const { resource, setResourceSearch: setResSearch, setResourceType: setTypeFilter, resetResources } = useUptimeUrlState() const { search: resSearch, type: typeFilter, error: resourceError } = resource @@ -944,32 +972,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
{/* ── Alert banner ──────────────────────────────────────────────────── */} - {alerts.length > 0 && ( - - - - {alerts.length} {alerts.length === 1 ? "сервер требует внимания" : "сервера требуют внимания"} - - -
- {alerts.map(r => { - const s = r.server - const issues: string[] = [] - if (r.cpu >= 85) issues.push(`CPU ${r.cpu}%`) - if (r.ramPct >= 85) issues.push(`RAM ${r.ramPct}%`) - if (r.hddPct >= 85) issues.push(`HDD ${r.hddPct}%`) - if ((r.temp ?? 0) >= 70) issues.push(`${r.temp}°C`) - return ( - - - {s.name} — {issues.join(", ")} - - ) - })} -
-
-
- )} + { test.setTimeout(90000) @@ -44,8 +45,35 @@ test("1000 ресурсов: страницы, поиск, обновление for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "…", exact: true }).last().click() await page.getByRole("button", { name: "Перейти на страницу 20", exact: true }).click() await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible() + resources = resources.map(resource => ({ ...resource, cpu: 90 })) + await page.getByRole("button", { name: "Обновить данные", exact: true }).click() + const warnings = page.getByRole("alert").filter({ hasText: "Серверов с предупреждениями: 1000" }) + await expect(warnings).toBeVisible() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(5) + console.log(JSON.stringify({ warningRows: 1000, warningNodes: await warnings.locator("*").count(), domNodes: await page.locator("*").count() })) + expect(await page.locator("*").count()).toBeLessThan(3500) + const summary = warnings.locator("summary") + await summary.focus() + await page.keyboard.press("Enter") + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(55) + await warnings.getByRole("button", { name: "Показать ещё 50", exact: true }).click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(105) + await summary.click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(5) + await summary.click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(55) + await summary.click() + await page.setViewportSize({ width: 320, height: 900 }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + const audit = await page.evaluate(async () => { + const engine = (window as unknown as { axe: typeof axe }).axe + const root = document.querySelector("[data-resource-warning]")!.closest("[role=alert]")! + return engine.run(root, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } }) + }) + await info.attach("resource-warning-axe", { body: JSON.stringify(audit), contentType: "application/json" }) + expect(audit.violations.map(violation => ({ id: violation.id, nodes: violation.nodes.map(node => node.target) }))).toEqual([]) resources = resources.slice(0, 1) await page.getByRole("button", { name: "Обновить данные", exact: true }).click() await expect(rows).toHaveCount(1)