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 00000000..984dffde
Binary files /dev/null and b/docs/ux-ui/screens/ospf-neighbors-table-320.png differ
diff --git a/tests/e2e/large-ospf-neighbors.spec.ts b/tests/e2e/large-ospf-neighbors.spec.ts
index 467397e1..c8b51939 100644
--- a/tests/e2e/large-ospf-neighbors.spec.ts
+++ b/tests/e2e/large-ospf-neighbors.spec.ts
@@ -1,30 +1,65 @@
import { test, expect } from "@playwright/test"
import { writeFileSync } from "node:fs"
+import axe from "axe-core"
import { ospfCatalogSchema } from "../../lib/ospf-response"
test("OSPF1000 соседей: профиль всего экрана с topology", async ({ page }, info) => {
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([])
})