From 1a82c1b41f1833e22f6ddfafb207cf5b49291956 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 17:09:16 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D1=83=20=D1=81=D0=BE=D1=81=D0=B5=D0=B4=D0=B5=D0=B9=20OSPF=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20URL=20=D1=83=D0=B7=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-grids/ospf-neighbors-data-grid.tsx | 43 ++++++++++++--- docs/ux-ui-performance.md | 7 +-- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 +++ docs/ux-ui-stage-12.md | 9 ++++ .../ospf-neighbors-table-2026-10-04.json | 9 ++++ .../screens/ospf-neighbors-table-320.png | Bin 0 -> 37071 bytes tests/e2e/large-ospf-neighbors.spec.ts | 49 +++++++++++++++--- 8 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 docs/ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json create mode 100644 docs/ux-ui/screens/ospf-neighbors-table-320.png diff --git a/components/data-grids/ospf-neighbors-data-grid.tsx b/components/data-grids/ospf-neighbors-data-grid.tsx index 4affa557..02493b9c 100644 --- a/components/data-grids/ospf-neighbors-data-grid.tsx +++ b/components/data-grids/ospf-neighbors-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, useRef } from "react" import { type ColumnDef, useTable, @@ -11,6 +11,7 @@ import type { OspfNeighborState } from "@/lib/ospf-state" import { cn } from "@/lib/utils" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { + DATA_GRID_CONTAINER_CLASS, DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, @@ -65,12 +66,13 @@ function OspfNeighborsDataGrid({ () => [ { id: "localLabel", + size: 144, accessorKey: "localLabel", header: ({ column }) => ( ), cell: ({ row }) => ( - onSelect ? : {row.original.localLabel} + onSelect ? : {row.original.localLabel} ), meta: { headerTitle: "Роутер", @@ -80,6 +82,7 @@ function OspfNeighborsDataGrid({ }, { id: "localIface", + size: 160, accessorKey: "localIface", header: ({ column }) => , cell: ({ row }) => ( @@ -95,6 +98,7 @@ function OspfNeighborsDataGrid({ }, { id: "remote", + size: 192, header: () => ( Сосед (Router ID) ), @@ -116,6 +120,7 @@ function OspfNeighborsDataGrid({ }, { id: "area", + size: 176, accessorKey: "area", header: ({ column }) => , cell: ({ row }) => ( @@ -129,6 +134,7 @@ function OspfNeighborsDataGrid({ }, { id: "state", + size: 128, accessorKey: "state", header: ({ column }) => , cell: ({ row }) => ( @@ -149,6 +155,7 @@ function OspfNeighborsDataGrid({ }, { id: "cost", + size: 80, accessorKey: "cost", header: ({ column }) => , cell: ({ row }) => ( @@ -162,6 +169,7 @@ function OspfNeighborsDataGrid({ }, { id: "uptime", + size: 112, accessorKey: "uptime", header: ({ column }) => , cell: ({ row }) => ( @@ -177,6 +185,7 @@ function OspfNeighborsDataGrid({ }, { id: "priority", + size: 64, accessorKey: "priority", header: ({ column }) => , cell: ({ row }) => ( @@ -202,7 +211,9 @@ function OspfNeighborsDataGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["localLabel"], end: [] } }, data: neighbors, columns, getRowId: (row) => row.id, @@ -210,6 +221,21 @@ function OspfNeighborsDataGrid({ state: { rowSelection }, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(neighbors.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [neighbors.length, pageIndex, pageSize, table]) + const requestedNode = useRef(null) + useEffect(() => { + if (!selectedRouterId) { requestedNode.current = null; return } + if (requestedNode.current === selectedRouterId) return + const index = table.getSortedRowModel().rows.findIndex(({ original: n }) => n.localRouter === selectedRouterId || n.remoteRouter === selectedRouterId || selectedRouterId === `remote-${n.remoteRouterId}`) + if (index < 0) return + requestedNode.current = selectedRouterId + table.setPageIndex(Math.floor(index / pageSize)) + }, [selectedRouterId, neighbors, pageSize, table]) + if (neighbors.length === 0) { return ( 50 || pageSize !== 50} onRowClick={(row) => { onSelect?.(selectedRouterId === row.localRouter ? null : row.localRouter) }} @@ -235,8 +262,12 @@ function OspfNeighborsDataGrid({ highlightRouterId && "[&: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 e82376ed..9f898543 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -15,13 +15,12 @@ ## Оставшиеся специализированные grids -Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —10 файлов после12.3.45. Это не10 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API. +Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —9 файлов после12.3.49. Это не9 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API. | Файл | Источник и необходимая проверка | | --- | --- | | [data-collection-scheduler-data-grid.tsx](../components/data-grids/data-collection-scheduler-data-grid.tsx) | 11 фиксированных SCHEDULER_JOB_KEYS (lib/scheduler-settings.ts); UI map этих ключей. Fixture1000 не соответствует источнику. | | [gre-pools-data-grid.tsx](../components/data-grids/gre-pools-data-grid.tsx) | В live displayPools=[]; только mockGrePools в demo. Приоритета live1000 нет. | -| [ospf-neighbors-data-grid.tsx](../components/data-grids/ospf-neighbors-data-grid.tsx) | Полный /ospf/catalog; одновременно SVG topology с distinct router IDs. Нужен профиль всего экрана, не только таблицы. | | [probes-schedule-data-grid.tsx](../components/data-grids/probes-schedule-data-grid.tsx) | Только demo ScheduleTab/INIT_RULES3; live использует ProbesSpeedSchedule. Demo список расширяется пользователем; не доказательство live API. | | [route-optimizer-comm-recs-data-grid.tsx](../components/data-grids/route-optimizer-comm-recs-data-grid.tsx) | Live commRecs slice(0,24) на HomeCard, не на страницу. Проверить количество HomeCard и честность ограничения рекомендаций. | | [route-optimizer-full-routes-data-grid.tsx](../components/data-grids/route-optimizer-full-routes-data-grid.tsx) | Live сочетания WAN×jumpHost×exitNode на HomeCard; ограничение DOM не устраняет стоимость расчёта полного массива. | @@ -30,7 +29,7 @@ | [subusers-data-grid.tsx](../components/data-grids/subusers-data-grid.tsx) | Поиск импортов/JSX в app/components не нашёл потребителя: компонент экспортируется, но не подключён. Не доказывает дефект экрана /users. | | [traffic-flows-data-grid.tsx](../components/data-grids/traffic-flows-data-grid.tsx) | FlowAnalyticsPanel/conversationsList: backend traffic-flow-analytics.ts top=min(50,max(10,settings.topN)), slice(0,top). Это top slice, не полный каталог/серверная pagination. Проверить сообщение об ограничении50 и связывание с графиками. | -Очередь: Firewall trace/cleanup → OSPF таблица+topology → optimizer preview/комбинаторика. Scheduler11, traffic cap50 и commRecs24 проверять на реальной границе, не искусственно подставлять1000 в недоступный DTO. Demo и неиспользуемый Subusers отделены от live приёмки. Все новые профили должны сохранять search/sort/refresh/clamp, точный target и права,320px и URL scope. +Очередь: OSPF topology/node details → optimizer preview/комбинаторика. Scheduler11, traffic cap50 и commRecs24 проверять на реальной границе, не искусственно подставлять1000 в недоступный DTO. Demo и неиспользуемый Subusers отделены от live приёмки. Все новые профили должны сохранять search/sort/refresh/clamp, точный target и права,320px и URL scope. Принятые после ранней карты: OSPF routes/BFD, WireGuard interfaces/peers/detail, VXLAN/GRE tunnels. Pagination сохраняет полный DTO в памяти и не доказывает серверную производительность. Текущий журнал: [ux-ui-progress.md](ux-ui-progress.md). @@ -93,3 +92,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.47: Firewall draft/URL/list filter/focus/lint cleanup завершены,26 main/context4/type/полный ESLint страницы/build прошли.10 manualPagination:true остаются; далее полный OSPF1000 baseline, включая SVG topology. 12.3.48: OSPF1000 neighbors/1000 interfaces/1001 nodes,583875 JSON bytes →1000 rows/14012 SVG descendants/32635 page nodes/6760ms dev ready.1 browser/test lint прошли, исходные профиль и desktop/mobile snapshots сохранены. Граф перекрывается и не принят. Далее12.3.49 таблица,12.3.50 topology;10 manualPagination:true остаются. + +12.3.49: OSPF1000 neighbors→50 rows,1001 SVG nodes/12015 descendants/13597 page nodes/2827ms dev ready. URL page20/search/global Cost sort/refresh/cache/clamp/320px/axe приняты;22 browser/type/lint/build. Весь граф не принят, KPI1000 переносится на320px, большой node detail вне scope; далее12.3.50–51.9 manualPagination:true остаются. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 90a593b1..a8f637b0 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 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. 12.3.28 принят локально:WireGuard interfaces1000/50 DOM/pinning/actions/cache/29+1 browser/build. 12.3.29 принят локально:WG peers1000/50 DOM/pinning/точный delete target/29+3 browser/build. 12.3.30 принят локально:WG detail1000/50 DOM/320px/клавиатура/31+1 browser/build. 12.3.31 принят локально:WG handshake без ложного online/unknown/7+1 browser/build. 12.3.32 принят локально:WG URL search/tab/status/reload/история/34+4 browser/unit/build. 12.3.33 принят локально:WG strict read/cache/freshness/unknown/40+7 browser/context8/build. 12.3.34 принят локально:VXLAN1000/50 DOM/pinning/export/22 browser/build. 12.3.35 принят локально:GRE1000/50 DOM/pinning/20+1 browser/build. 12.3.36 карта15 оставшихся источников обновлена. 12.3.37 top50 traffic/пустой локальный фильтр/перенос tabs принят в описанных границах. 12.3.38 Communities1000/50 DOM/19 browser/build принят локально. 12.3.39 clipboard ack/error/retry/unmount/5 browser/build принят локально. 12.3.40 recursive groups/endpoints1000/50 DOM/35 browser/build принят локально. 12.3.41 большая форма1000×1000/10 cards/полный PUT/22+1 browser/build принята локально. 12.3.42 Filters1000/50 DOM/полный PUT/10+1 browser/build принят локально. 12.3.43 Firewall1000/50 DOM/DnD/35+1 main/context4/build принят локально. 12.3.44 stale guard/29+2 main/context4/build принят локально;4 исходных lint errors страницы требуют cleanup. 12.3.45 сценарий1000/50 DOM/полный draft/verdict/6 browser/build принят локально. 12.3.46 trace999/50 steps/1293 nodes/epoch/6+1 browser/build принят локально. 12.3.47 Firewall forms/URL/focus/26 main/context4/lint/build принят локально; исходные lint errors устранены. 12.3.48 OSPF baseline32635 nodes/14012 SVG сохранён, performance и читаемость не приняты. Следующие12.3.49 neighbors pages50 и12.3.50 topology срез; затем optimizer и полная 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 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. 12.3.27 принят локально:WireGuard zero/missing counters/UI/backend mapper/29 browser/unit/builds. 12.3.28 принят локально:WireGuard interfaces1000/50 DOM/pinning/actions/cache/29+1 browser/build. 12.3.29 принят локально:WG peers1000/50 DOM/pinning/точный delete target/29+3 browser/build. 12.3.30 принят локально:WG detail1000/50 DOM/320px/клавиатура/31+1 browser/build. 12.3.31 принят локально:WG handshake без ложного online/unknown/7+1 browser/build. 12.3.32 принят локально:WG URL search/tab/status/reload/история/34+4 browser/unit/build. 12.3.33 принят локально:WG strict read/cache/freshness/unknown/40+7 browser/context8/build. 12.3.34 принят локально:VXLAN1000/50 DOM/pinning/export/22 browser/build. 12.3.35 принят локально:GRE1000/50 DOM/pinning/20+1 browser/build. 12.3.36 карта15 оставшихся источников обновлена. 12.3.37 top50 traffic/пустой локальный фильтр/перенос tabs принят в описанных границах. 12.3.38 Communities1000/50 DOM/19 browser/build принят локально. 12.3.39 clipboard ack/error/retry/unmount/5 browser/build принят локально. 12.3.40 recursive groups/endpoints1000/50 DOM/35 browser/build принят локально. 12.3.41 большая форма1000×1000/10 cards/полный PUT/22+1 browser/build принята локально. 12.3.42 Filters1000/50 DOM/полный PUT/10+1 browser/build принят локально. 12.3.43 Firewall1000/50 DOM/DnD/35+1 main/context4/build принят локально. 12.3.44 stale guard/29+2 main/context4/build принят локально;4 исходных lint errors страницы требуют cleanup. 12.3.45 сценарий1000/50 DOM/полный draft/verdict/6 browser/build принят локально. 12.3.46 trace999/50 steps/1293 nodes/epoch/6+1 browser/build принят локально. 12.3.47 Firewall forms/URL/focus/26 main/context4/lint/build принят локально; исходные lint errors устранены. 12.3.48 OSPF baseline32635 nodes/14012 SVG сохранён, performance и читаемость не приняты. 12.3.49 neighbors pages50/22 browser/build приняты локально, полный граф не принят. Следующие12.3.50 topology срез/KPI и12.3.51 node details; затем optimizer и полная 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 cc0c5f55..33e88cb9 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -31,6 +31,14 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13–12.3.25 приняты локально в описанных границах;12.4.4 context213 и12.4.5 main1110 прошли, 12.3.26–12.3.33 приняты локально; 12.3.34 VXLAN и12.3.35 GRE tunnels приняты; остаются network grids/общая регрессия после delta/внешняя приёмка | +## 12.3.49 — большая таблица соседей OSPF (2026-10-04) + +ReUI pagination50/clamp/ScrollArea, закреплённый local router и кнопка узла44px. Новый/первичный URL node находит строку в глобальной sorted model и открывает её страницу; обычная pagination/refresh не навязывает возврат к выбранному узлу. Search/sort используют весь срез, selection не привязан к странице. Unknown/cost/null/права сохранены. + +22/22 browser (large/data/layout/links/values) и финальные large повторы прошли;1000/583875 JSON bytes →50 table rows,1001 graph nodes/12015 SVG descendants/13597 page nodes/2827ms dev ready. URL remote1000→page20, refresh/ручная page19, search/reload/сохранение keep, global Cost sort/cache503/unknown/clamp1000→1/no writes,320px/pinning/axe проверены. TypeScript без incremental/ESLint grid+test/production build прошли. [Профиль таблицы](ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json), [таблица320px](ux-ui/screens/ospf-neighbors-table-320.png) просмотрены. SVG количество зависит также от animation/reduced-motion состояния; baseline14k и текущие12k не означают изменение алгоритма графа. Dev latency не считается production benchmark. + +Осталось9 manualPagination:true specialized grids. Весь экран не принят: граф1001 перекрывается; KPI1000 на320px переносит число; NodeDetailPanel локального узла рендерит все связи. Следующий блок12.3.50: читаемый ограниченный срез topology/явные counts/поиск и выбор любого узла/URL/полный источник, исправить отображение чисел. Затем12.3.51 большие node details, optimizer и общая main/context после delta. Внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.48 — исходный профиль OSPF соседей и topology (2026-10-04) Fixture полного DTO:1000 neighbors/1000 interfaces/одна instance,1001 distinct graph nodes;583875 JSON bytes.1/1 browser и ESLint теста прошли. Исходный экран:1000 table rows/14012 SVG descendants/32635 page nodes/6760ms dev ready.320px document width320, но это не доказательство читаемости. [Профиль](ux-ui/acceptance/ospf-neighbors-baseline-2026-10-04.json), [desktop](ux-ui/screens/ospf-neighbors-baseline-1440.png), [mobile](ux-ui/screens/ospf-neighbors-baseline-320.png) сохранены и просмотрены: узлы на фиксированной окружности перекрывают друг друга, граф не принят. Latency одного dev прогона не является production benchmark. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index b8cc6e0f..a5c9090b 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -585,3 +585,12 @@ Fixture полного DTO:1000 neighbors/1000 interfaces/одна instance,1001 Продуктовый код не менялся; build повторно не нужен для теста/документации. Следующий блок12.3.49: client pages50 OSPF neighbors/полный поиск и sort/URL node/refresh/cache/clamp/320px. Принимать таблицу отдельно, не объявлять готовность всего экрана: граф14012 SVG требует12.3.50 — читаемый ограниченный срез topology с явными counts/выбором узла/переходами ко всем данным. Затем optimizer и общая main/context после delta.10 manualPagination:true остаются; внешняя12.5 отдельно. Коммит локальный без push. + +## 12.3.49 — большая таблица соседей OSPF (2026-10-04) + +ReUI pagination50/clamp/ScrollArea, закреплённый local router и кнопка узла44px. Новый/первичный URL node находит строку в глобальной sorted model и открывает её страницу; обычная pagination/refresh не навязывает возврат к выбранному узлу. Search/sort используют весь срез, selection не привязан к странице. Unknown/cost/null/права сохранены. + +22/22 browser (large/data/layout/links/values) и финальные large повторы прошли;1000/583875 JSON bytes →50 table rows,1001 graph nodes/12015 SVG descendants/13597 page nodes/2827ms dev ready. URL remote1000→page20, refresh/ручная page19, search/reload/сохранение keep, global Cost sort/cache503/unknown/clamp1000→1/no writes,320px/pinning/axe проверены. TypeScript без incremental/ESLint grid+test/production build прошли. [Профиль таблицы](ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json), [таблица320px](ux-ui/screens/ospf-neighbors-table-320.png) просмотрены. SVG количество зависит также от animation/reduced-motion состояния; baseline14k и текущие12k не означают изменение алгоритма графа. Dev latency не считается production benchmark. + +Осталось9 manualPagination:true specialized grids. Весь экран не принят: граф1001 перекрывается; KPI1000 на320px переносит число; NodeDetailPanel локального узла рендерит все связи. Следующий блок12.3.50: читаемый ограниченный срез topology/явные counts/поиск и выбор любого узла/URL/полный источник, исправить отображение чисел. Затем12.3.51 большие node details, optimizer и общая main/context после delta. Внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/docs/ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json b/docs/ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json new file mode 100644 index 00000000..c2c7d1eb --- /dev/null +++ b/docs/ux-ui/acceptance/ospf-neighbors-table-2026-10-04.json @@ -0,0 +1,9 @@ +{ + "count": 1000, + "bytes": 583875, + "rows": 50, + "graphNodes": 1001, + "svgNodes": 12015, + "pageNodes": 13597, + "readyMs": 2827 +} \ No newline at end of file diff --git a/docs/ux-ui/screens/ospf-neighbors-table-320.png b/docs/ux-ui/screens/ospf-neighbors-table-320.png new file mode 100644 index 0000000000000000000000000000000000000000..984dffdeb65c7fc3ac18ecd0f6b04b268b0e6912 GIT binary patch literal 37071 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 { test.setTimeout(90000) const host = { serverId: 1, serverName: "Роутер 1", serverSite: "", serverCountry: "RU" } - const neighbors = Array.from({ length: 1000 }, (_, i) => ({ ...host, id: `n${i + 1}`, address: `10.${Math.floor(i / 256)}.${i % 256}.1`, routerId: `10.${Math.floor(i / 256)}.${i % 256}.1`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: `ether${i + 1}`, state: "Full", uptime: "1h", stateChanges: 0, priority: 1 })) + let neighbors = Array.from({ length: 1000 }, (_, i) => ({ ...host, id: `n${i + 1}`, address: `10.${Math.floor(i / 256)}.${i % 256}.1`, routerId: `10.${Math.floor(i / 256)}.${i % 256}.1`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: `ether${i + 1}`, state: "Full", uptime: "1h", stateChanges: 0, priority: 1 })) const interfaces = neighbors.map((n, i) => ({ ...host, id: `i${i + 1}`, instance: "default", area: "backbone", areaId: "0.0.0.0", interface: n.interface, cost: i + 1, type: "broadcast", disabled: false, inactive: false, priority: 1, helloInterval: 10, deadInterval: 40, useBfd: false })) const catalog = ospfCatalogSchema.parse({ neighbors, interfaces, instances: [{ ...host, id: "instance1", name: "default", routerId: "192.0.2.1", version: 2, disabled: false, inactive: false, redistribute: "" }], bfdSessions: [], routes: [], failures: [] }) + let failed = false, reads = 0 + 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") { reads++; return r.fulfill(failed ? { status: 503, json: { error: "Каталог OSPF недоступен1000" } } : { json: { ...catalog, neighbors } }) } return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/ospf/catalog" ? catalog : p === "/api/servers" ? [{ id: 1, name: "Роутер 1", host: "192.0.2.1", enabled: true, country: "RU", status: null }] : {} }) }) await page.setViewportSize({ width: 1440, height: 1000 }) - const start = Date.now(); await page.goto("/ospf?tab=neighbors&server=1&keep=1") + const start = Date.now(); await page.goto("/ospf?tab=neighbors&server=1&node=remote-10.3.231.1&keep=1") const rows = page.locator("tr[data-row-id]") - await expect(rows).toHaveCount(1000) + await expect(rows).toHaveCount(50) + await expect(rows.first()).toHaveAttribute("data-row-id", "1-n951") await expect(page.locator("[data-ospf-node]")).toHaveCount(1001) const profile = { count: neighbors.length, bytes: Buffer.byteLength(JSON.stringify(catalog)), rows: await rows.count(), graphNodes: await page.locator("[data-ospf-node]").count(), svgNodes: await page.getByRole("group", { name: "Топология OSPF", exact: true }).locator("*").count(), pageNodes: await page.locator("*").count(), readyMs: Date.now() - start } - console.log("OSPF neighbors baseline", JSON.stringify(profile)) + console.log("OSPF neighbors table", JSON.stringify(profile)) writeFileSync(info.outputPath("ospf-neighbors-profile.json"), JSON.stringify(profile, null, 2)) await info.attach("profile", { body: JSON.stringify(profile), contentType: "application/json" }) await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) - await page.screenshot({ path: info.outputPath("ospf-neighbors-baseline-1440.png"), animations: "disabled" }) + await page.screenshot({ path: info.outputPath("ospf-neighbors-table-1440.png"), animations: "disabled" }) + const next = page.getByRole("button", { name: "Следующая страница", exact: true }) + await expect(next).toBeDisabled() + const before = reads; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => reads).toBeGreaterThan(before); await expect(rows.first()).toHaveAttribute("data-row-id", "1-n951") + await page.getByRole("button", { name: "Предыдущая страница", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "1-n901") + const search = page.getByPlaceholder("Поиск в текущем разделе OSPF…") + await search.fill("10.3.231.1"); await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000") + await expect(page).toHaveURL(url => url.searchParams.get("q") === "10.3.231.1" && 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: "Cost", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000") + await next.click(); const previous = await rows.first().getAttribute("data-row-id") + failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.getByRole("alert").filter({ hasText: "Не удалось загрузить OSPF" })).toContainText("Показаны последние полученные данные") + await expect(rows.first()).toHaveAttribute("data-row-id", previous!) + await expect(rows.first()).toContainText("Неизвестно") await page.setViewportSize({ width: 320, height: 1000 }) - console.log("OSPF neighbors baseline mobileWidth", await page.evaluate(() => document.documentElement.scrollWidth)) - await page.screenshot({ path: info.outputPath("ospf-neighbors-baseline-320.png"), animations: "disabled" }) + const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]') + await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + const pinned = rows.first().getByRole("cell").first() + expect(await pinned.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) + await rows.first().scrollIntoViewIfNeeded() + 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.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([]) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320) + await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) + await page.screenshot({ path: info.outputPath("ospf-neighbors-table-320.png"), animations: "disabled" }) + failed = false; neighbors = [neighbors[999]]; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(rows).toHaveCount(1); await expect(rows.first()).toHaveAttribute("data-row-id", "1-n1000") + expect(writes).toEqual([]) })