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)