diff --git a/components/data-grids/ospf-bfd-data-grid.tsx b/components/data-grids/ospf-bfd-data-grid.tsx index 6f40f914..691f2f08 100644 --- a/components/data-grids/ospf-bfd-data-grid.tsx +++ b/components/data-grids/ospf-bfd-data-grid.tsx @@ -1,8 +1,8 @@ "use client" -import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" +import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo } from "react" +import { useEffect, useMemo } from "react" import { type ColumnDef, useTable, @@ -76,6 +76,7 @@ function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) { () => [ { id: "serverLabel", + size: 128, accessorKey: "serverLabel", header: ({ column }) => ( @@ -84,7 +85,7 @@ function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) { const b = row.original return (
- {b.serverLabel} + {b.serverLabel} {b.multihop === null && Режим неизвестен} {b.multihop === true && ( @@ -254,6 +255,7 @@ function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) { }, { id: "stateChanges", + size: 112, accessorKey: "stateChanges", header: ({ column }) => ( @@ -273,12 +275,20 @@ function OspfBfdDataGrid({ sessions }: OspfBfdDataGridProps) { const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["serverLabel"], end: [] } }, data: sessions, columns, getRowId: (row) => row.id, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(sessions.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [sessions.length, pageIndex, pageSize, table]) + if (sessions.length === 0) { return ( 50 || pageSize !== 50} tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row hover:bg-muted/30" }} - tableLayout={{ dense: true }} - /> + tableLayout={{ dense: true, columnsPinnable: true }} + > + + + + ) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 5907a5fa..44d5d39a 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -76,3 +76,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.21: history200 остаётся50 DOM rows/2367 nodes/1150ms dev ready. Strict DTO и null rates приняты от pure DB mapper до grid/KPI/card;60+5 browser и unit4/builds. Это не проверка реального PostgreSQL/RouterOS. 12.3.24:OSPF routes1000/200347 JSON bytes —50 DOM rows/1356 page nodes/930ms dev ready. Источник /ospf/catalog объединяет полный RouterOS ipRoutes без page/limit. Whole-catalog search/URL/reload/global Cost sort/page20/refresh/503 cache/1000→1/320px/видимость области/axe приняты;22+1 browser/type/lint/build. Полный DTO остаётся в памяти; скорость RouterOS не измерена. Осталось20 manualPagination:true specialized grids, включая demo/bounded источники; это не20 доказанных дефектов. + +12.3.25:BFD1000/323133 JSON bytes —50 DOM rows/1956 page nodes/983ms dev ready. Источник /ospf/catalog объединяет полный RouterOS bfdSessions без page/limit. Whole-catalog search/URL/reload/global Rx sort/page20/refresh/503 cache/1000→1/320px/pinning/axe приняты;12 browser/type/lint/build. Полный DTO остаётся в памяти; скорость RouterOS не измерялась. Осталось19 manualPagination:true specialized grids, включая demo/bounded источники. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 241b18d0..bf066272 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. Следующий блок12.3.25:BFD sessions большого каталога; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. Следующий блок12.4.4:общая context матрица213, затем12.4.5 main текущего HEAD и12.3.26 оставшиеся network grids; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index b1f14e04..be63f368 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,14 @@ +## 12.3.25 — большой каталог BFD-сессий (2026-10-04) + +Источник /ospf/catalog читает и объединяет полный RouterOS bfdSessions без pagination cap. OspfBfdDataGrid использует client pagination50, сохранение страницы при refresh и clamp при сокращении/поиске. Роутер закреплён штатным start/end pinning, горизонтальная прокрутка через DataGridScrollArea; последняя метрика полностью видима на320px. Метрики и режимы unknown/null остаются прежними, сетевые операции не изменены. + +1000 BFD/323133 JSON bytes:50 DOM rows/1956 page nodes/983ms dev ready.12 browser прошли:page20/refresh/global Rx sort/поиск по всему remoteAddr каталогу/URL/reload/503 cache/unknown/recovery1000→1/320px/axe, включая прежние null/zero и layout сценарии. Lint/typecheck и production build прошли, мобильный снимок просмотрен. Полные DTO остаются в памяти; реальный RouterOS опрос не проверен. Осталось19 manualPagination:true specialized grids, включая bounded/demo, это не19 доказанных дефектов. + +Следующий блок12.4.4: повтор общей context матрицы213 scenarios после последних delta. Затем12.4.5 общий main прогон текущего HEAD и12.3.26 оставшиеся network grids. Внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.24 — большой каталог OSPF-маршрутов (2026-10-04) Аудит источника: /ospf/catalog опрашивает полный RouterOS ipRoutes и объединяет маршруты устройств без page/limit. Grid переведён на штатную TanStack v9 client pagination50; сортировка применяется ко всему отфильтрованному каталогу. Страница сохраняется при refresh и ограничивается при сокращении списка/поиске. Назначение закреплено штатным columnPinning start/end, горизонтальная прокрутка использует DataGridScrollArea. Подобраны ширины, чтобы значение области не перекрывалось на320px; длинный префикс переносится. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 1e52f375..3d437730 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -353,3 +353,12 @@ Backend заменяет каталог в одной Drizzle transaction с pg_ Следующий блок12.3.25: BFD sessions большого каталога OSPF,50 DOM rows/поиск/sort/cache/320px. После него оставшиеся network grids и общая матрица12.4; внешняя12.5 отдельно. Коммит локальный без push. + +## 12.3.25 — большой каталог BFD-сессий (2026-10-04) + +Источник /ospf/catalog читает и объединяет полный RouterOS bfdSessions без pagination cap. OspfBfdDataGrid использует client pagination50, сохранение страницы при refresh и clamp при сокращении/поиске. Роутер закреплён штатным start/end pinning, горизонтальная прокрутка через DataGridScrollArea; последняя метрика полностью видима на320px. Метрики и режимы unknown/null остаются прежними, сетевые операции не изменены. + +1000 BFD/323133 JSON bytes:50 DOM rows/1956 page nodes/983ms dev ready.12 browser прошли:page20/refresh/global Rx sort/поиск по всему remoteAddr каталогу/URL/reload/503 cache/unknown/recovery1000→1/320px/axe, включая прежние null/zero и layout сценарии. Lint/typecheck и production build прошли, мобильный снимок просмотрен. Полные DTO остаются в памяти; реальный RouterOS опрос не проверен. Осталось19 manualPagination:true specialized grids, включая bounded/demo, это не19 доказанных дефектов. + +Следующий блок12.4.4: повтор общей context матрицы213 scenarios после последних delta. Затем12.4.5 общий main прогон текущего HEAD и12.3.26 оставшиеся network grids. Внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/docs/ux-ui/screens/large-ospf-bfd-320.png b/docs/ux-ui/screens/large-ospf-bfd-320.png new file mode 100644 index 00000000..2a2b0e14 Binary files /dev/null and b/docs/ux-ui/screens/large-ospf-bfd-320.png differ diff --git a/tests/e2e/large-ospf-bfd.spec.ts b/tests/e2e/large-ospf-bfd.spec.ts new file mode 100644 index 00000000..53a928a2 --- /dev/null +++ b/tests/e2e/large-ospf-bfd.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from "@playwright/test" +import { ospfCatalogSchema } from "../../lib/ospf-response" +import axe from "axe-core" +test("OSPF BFD1000: catalog search/sort, pagination/cache/refresh and320px", async ({ page }, info) => { + test.setTimeout(90000) + const server = { id: 1, name: "Роутер 1", host: "192.0.2.1", country: "RU", enabled: true, status: null } + let sessions = Array.from({ length: 1000 }, (_, i) => ({ id: "*" + (i + 1), serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU", localAddr: "192.0.2.1", remoteAddr: "10." + Math.floor(i / 256) + "." + (i % 256) + ".1", interface: "ether" + (i + 1), state: "Up", uptime: "1h", multihop: false, multiplier: 3, txInterval: 100, rxInterval: 100, holdTime: 300, packetsRx: i + 1, packetsTx: 0, stateChanges: i + 1 })) + let calls = 0, failed = false; const writes: string[] = [] + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const p = new URL(r.request().url()).pathname + if (r.request().method() !== "GET") writes.push(p) + if (p === "/api/ospf/catalog") { calls++; return r.fulfill(failed ? { status: 503, json: { error: "BFD catalog unavailable" } } : { json: ospfCatalogSchema.parse({ neighbors: [], interfaces: [], instances: [], routes: [], bfdSessions: sessions, failures: [] }) }) } + return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/servers" ? [server] : {} }) + }) + const started = Date.now(); await page.goto("/ospf?tab=bfd&server=1&keep=1") + const rows = page.locator("tr[data-row-id]"); await expect(rows).toHaveCount(50) + const profile = { count: sessions.length, jsonBytes: Buffer.byteLength(JSON.stringify(sessions)), renderedRows: await rows.count(), nodes: await page.locator("*").count(), readyMs: Date.now() - started } + console.log("BFD profile", JSON.stringify(profile)); await info.attach("profile", { body: JSON.stringify(profile), contentType: "application/json" }) + const next = page.getByRole("button", { name: "Следующая страница", exact: true }) + for (let i=0;i<19;i++) await next.click() + await expect(next).toBeDisabled(); await expect(rows.first()).toHaveAttribute("data-row-id", "1-*951") + const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => calls).toBeGreaterThan(before); await expect(rows.first()).toHaveAttribute("data-row-id", "1-*951") + const search = page.getByPlaceholder("Поиск в текущем разделе OSPF…") + await search.fill(sessions[999].remoteAddr); await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "1-*1000") + await expect(page).toHaveURL(url => url.searchParams.get("q") === sessions[999].remoteAddr && url.searchParams.get("keep") === "1") + await page.reload(); await expect(rows).toHaveCount(1); await search.fill(""); await expect(rows).toHaveCount(50) + await page.getByRole("table").getByRole("button", { name: "Пакеты Rx", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "1-*1000") + await next.click(); const previous = await rows.first().getAttribute("data-row-id") + failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click() + const alert = page.getByRole("alert").filter({ hasText: "Не удалось загрузить OSPF" }) + await expect(alert).toContainText("Показаны последние полученные данные"); await expect(rows).toHaveCount(50); await expect(rows.first()).toHaveAttribute("data-row-id", previous!) + await expect(rows.first()).toContainText("Неизвестно") + await page.setViewportSize({ width: 320, height: 1000 }) + const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]') + await expect(scroll).toHaveCount(1); await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0) + const cells = rows.first().getByRole("cell") + expect(await cells.first().evaluate(el => { const a=el.getBoundingClientRect(),v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect();return a.left>=v.left-1&&a.right<=v.right+1 })).toBe(true) + expect(await cells.last().locator("span").evaluate(el => {const a=el.getBoundingClientRect(),v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(),p=el.closest("tr")!.querySelector("td")!.getBoundingClientRect();return a.left>=p.right-1&&a.right<=v.right+1})).toBe(true) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations).toEqual([]) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }) + await page.getByRole("table").getByRole("button", { name: "Роутер", exact: true }).scrollIntoViewIfNeeded() + await page.screenshot({ path: info.outputPath("ospf-bfd-320.png"), animations: "disabled" }) + failed = false; sessions = [sessions[999]]; await alert.getByRole("button", { name: "Повторить загрузку OSPF" }).click() + await expect(rows).toHaveCount(1); await expect(alert).toHaveCount(0); await expect(rows.first()).toHaveAttribute("data-row-id", "1-*1000") + expect(writes).toEqual([]) +})