diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index be356f6b..27eae6b4 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -1160,7 +1160,7 @@ function ScenarioSheet({ open, onClose, initial, onSave }: { return ( { if (!v) onClose() }}> - + {isNew ? "Новый сценарий" : `Редактировать: ${initial?.name}`} Набор правил firewall/mangle/nat + тестовый пакет @@ -1552,11 +1552,13 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) { diff --git a/components/data-grids/firewall-scenario-rules-data-grid.tsx b/components/data-grids/firewall-scenario-rules-data-grid.tsx index a83a4c8c..07b886cd 100644 --- a/components/data-grids/firewall-scenario-rules-data-grid.tsx +++ b/components/data-grids/firewall-scenario-rules-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, @@ -10,6 +10,7 @@ import { import { ActionBadge, ChainBadge } from "@/components/data-grids/firewall-rules-data-grid" 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, @@ -74,6 +75,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "chain", + size: 112, accessorKey: "chain", header: () => Цепочка, enableSorting: false, @@ -86,6 +88,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "action", + size: 176, accessorKey: "action", header: () => Действие, enableSorting: false, @@ -94,6 +97,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "src", + size: 176, accessorKey: "src", header: () => Src, enableSorting: false, @@ -106,6 +110,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "dst", + size: 176, accessorKey: "dst", header: () => Dst, enableSorting: false, @@ -118,6 +123,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "port", + size: 112, accessorKey: "port", header: () => Порт, enableSorting: false, @@ -128,6 +134,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "iface", + size: 160, accessorKey: "iface", header: () => Iface, enableSorting: false, @@ -138,6 +145,7 @@ function FirewallScenarioRulesDataGrid({ }, { id: "comment", + size: 192, accessorKey: "comment", header: () => Комментарий, enableSorting: false, @@ -150,17 +158,18 @@ function FirewallScenarioRulesDataGrid({ }, { id: "actions", + size: 208, header: () => null, enableSorting: false, cell: ({ row }) => { const index = row.original._index - 1 return ( -
+
@@ -169,7 +178,7 @@ function FirewallScenarioRulesDataGrid({ onClick={() => onMoveUp(row.original.id)} aria-label={`Поднять правило ${index + 1}`} disabled={index === 0} - className="p-0.5 text-muted-foreground/40 hover:text-foreground disabled:opacity-20 transition-colors" + className="size-11 shrink-0 inline-flex items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:opacity-40 transition-colors" > ▲ @@ -178,7 +187,7 @@ function FirewallScenarioRulesDataGrid({ onClick={() => onMoveDown(row.original.id)} aria-label={`Опустить правило ${index + 1}`} disabled={index === rules.length - 1} - className="p-0.5 text-muted-foreground/40 hover:text-foreground disabled:opacity-20 transition-colors" + className="size-11 shrink-0 inline-flex items-center justify-center rounded text-muted-foreground hover:text-foreground disabled:opacity-40 transition-colors" > ▼ @@ -186,7 +195,7 @@ function FirewallScenarioRulesDataGrid({ type="button" onClick={() => onRemove(row.original.id)} aria-label={`Удалить правило ${index + 1}`} - className="p-0.5 ml-0.5 text-muted-foreground/40 hover:text-red-500 transition-colors" + className="size-11 shrink-0 inline-flex items-center justify-center rounded text-muted-foreground hover:text-red-500 transition-colors" > @@ -201,21 +210,35 @@ function FirewallScenarioRulesDataGrid({ const table = useTable({ features: dataGridFeatures, - manualPagination: true, + manualPagination: false, + autoResetPageIndex: false, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["index", "chain"], end: [] } }, manualSorting: true, data: indexedRules, columns, getRowId: (row) => row.id, }) + const { pageIndex, pageSize } = table.state.pagination + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(rules.length / pageSize) - 1) + if (pageIndex > lastPage) table.setPageIndex(lastPage) + }, [rules.length, pageIndex, pageSize, table]) + return ( 50 || pageSize !== 50} + tableLayout={{ dense: true, columnsPinnable: true }} tableClassNames={{ bodyRow: cn("group/row text-xs", "hover:bg-muted/20 transition-colors"), }} - /> + > + + + + ) } diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 59db4782..414e0ee5 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -15,12 +15,11 @@ ## Оставшиеся специализированные grids -Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —11 файлов после12.3.43. Это не11 доказанных дефектов: есть фиксированный источник, demo, неподключённый компонент и ограниченные срезы. Файлы ниже сопоставлены с действующими JSX-потребителями; ограничения установлены чтением кода, не нагрузочной приёмкой API. +Аудит12.3.36 от2026-10-04: точный поиск `manualPagination: true` в components/data-grids —10 файлов после12.3.45. Это не10 доказанных дефектов: есть фиксированный источник, 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 не соответствует источнику. | -| [firewall-scenario-rules-data-grid.tsx](../components/data-grids/firewall-scenario-rules-data-grid.tsx) | Локальный редактор сценария, динамический массив; не путать с live правилами. Проверить сохранение/симуляцию всего сценария. | | [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. | @@ -31,7 +30,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 stale guard/редактор сценария → OSPF таблица+topology → optimizer preview/комбинаторика. Scheduler11, traffic cap50 и commRecs24 проверять на реальной границе, не искусственно подставлять1000 в недоступный DTO. Demo и неиспользуемый Subusers отделены от live приёмки. Все новые профили должны сохранять search/sort/refresh/clamp, точный target и права,320px и URL scope. +Очередь: 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 routes/BFD, WireGuard interfaces/peers/detail, VXLAN/GRE tunnels. Pagination сохраняет полный DTO в памяти и не доказывает серверную производительность. Текущий журнал: [ux-ui-progress.md](ux-ui-progress.md). @@ -86,3 +85,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.43: Firewall1000/253248 JSON bytes →50 rows/3054 nodes/2082ms dev ready. Cross-page кнопки/точный RouterOS destination/DnD page2/403/search/cache/clamp/320px/axe проверены;35+1 main/context4/type/lint/build прошли. Stale operations после полного GET failure требуют12.3.44; большой multi-server sort отдельно не проверен.11 manualPagination:true остаются. 12.3.44: stale операции Firewall заблокированы до успешного read/после retry, ограничения12.3.43 по полному GET failure исправлены.29+2 main/context4/type/build прошли;4 исходных ESLint errors страницы подтверждены сравнением с HEAD.11 manualPagination:true остаются. + +12.3.45: локальный сценарий1000→50 rows/1847 modal nodes; pagination/full draft999/скрытый verdict#999/320px/axe проверены,6 browser/type/grid lint/build прошли. Трассировка999 остаётся15529 page nodes, требует12.3.46; ошибки записи localStorage не проверены.10 manualPagination:true остаются. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 8baf8574..a80856be 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: сценарий Firewall; затем OSPF/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/15529 nodes; затем lint cleanup/OSPF/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 2f2e8616..247dad0b 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.45 — большой редактор сценария Firewall (2026-10-04) + +ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain, действия44px без hover; номера и up/down края считаются по полному draft. ScenarioSheet320px занимает всю ширину viewport. Кнопки редактирования/удаления сценария получили44px и доступные имена. Локальный сценарий не отправляет операции RouterOS. + +Финальные6/6 browser (tables и large scenario) прошли.1000 правил →50 rows/1847 modal nodes. Проверены down50 через страницу/up51 обратно/delete51, полный сохранённый localStorage draft999/точный порядок/последнее правило неизменно,320px/полная кнопка44px/axe. Симуляция вычисляет скрытое последнее правило #999 и блокирует пакет; API writes0. TypeScript без incremental, ESLint grid/test и production build прошли. [Сценарий320px](ux-ui/screens/large-firewall-scenario-320.png) просмотрен. Четыре исходных lint errors страницы/2 warnings из12.3.44 остаются; отказ записи localStorage отдельно не проверен. + +Обнаружено: trace999 создаёт15529 page DOM nodes; ограничение редактора не означает принятие всего симулятора. Следующий блок12.3.46: страницы трассировки/полный verdict/переход к результату и сохранение отмены/повторного запуска. Затем Firewall lint cleanup, OSPF таблица+topology/optimizer.10 manualPagination:true specialized grids остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.44 — блокировка устаревших операций Firewall (2026-10-04) readTrusted отделён от текста dataError: после HTTP/DTO failure кэш сохраняется, но toggle/edit/delete/create/move/address-list/history restore заблокированы. Очистка ошибки при retry не разблокирует действия до корректного ответа. Существующие partial failures дополнительно сохраняют scoped блокировку; поздний read после мутации не сбрасывает подтверждение. Статус объясняет блокировку и сохранённый список, операции RouterOS не изменены. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 6d6095ac..dd162789 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -551,3 +551,12 @@ readTrusted отделён от текста dataError: после HTTP/DTO fail Следующий блок12.3.45: большой редактор локального сценария Firewall — pagination/глобальный порядок/сохранение всего draft/симуляция/320px. Затем OSPF таблица+topology/optimizer.11 manualPagination:true остаются. Общая main/context после delta, lint cleanup Firewall и внешняя12.5 отдельно. Коммит локальный без push. + +## 12.3.45 — большой редактор сценария Firewall (2026-10-04) + +ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain, действия44px без hover; номера и up/down края считаются по полному draft. ScenarioSheet320px занимает всю ширину viewport. Кнопки редактирования/удаления сценария получили44px и доступные имена. Локальный сценарий не отправляет операции RouterOS. + +Финальные6/6 browser (tables и large scenario) прошли.1000 правил →50 rows/1847 modal nodes. Проверены down50 через страницу/up51 обратно/delete51, полный сохранённый localStorage draft999/точный порядок/последнее правило неизменно,320px/полная кнопка44px/axe. Симуляция вычисляет скрытое последнее правило #999 и блокирует пакет; API writes0. TypeScript без incremental, ESLint grid/test и production build прошли. [Сценарий320px](ux-ui/screens/large-firewall-scenario-320.png) просмотрен. Четыре исходных lint errors страницы/2 warnings из12.3.44 остаются; отказ записи localStorage отдельно не проверен. + +Обнаружено: trace999 создаёт15529 page DOM nodes; ограничение редактора не означает принятие всего симулятора. Следующий блок12.3.46: страницы трассировки/полный verdict/переход к результату и сохранение отмены/повторного запуска. Затем Firewall lint cleanup, OSPF таблица+topology/optimizer.10 manualPagination:true specialized grids остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/docs/ux-ui/screens/large-firewall-scenario-320.png b/docs/ux-ui/screens/large-firewall-scenario-320.png new file mode 100644 index 00000000..c634053f Binary files /dev/null and b/docs/ux-ui/screens/large-firewall-scenario-320.png differ diff --git a/tests/e2e/large-firewall-scenario.spec.ts b/tests/e2e/large-firewall-scenario.spec.ts new file mode 100644 index 00000000..ce68e1a2 --- /dev/null +++ b/tests/e2e/large-firewall-scenario.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from "@playwright/test" +import axe from "axe-core" + +test("Сценарий1000: граница страниц, полный draft и симуляция скрытого правила", async ({ page }, info) => { + test.setTimeout(90000) + const packet = { chain: "forward", proto: "tcp", srcAddr: "10.10.0.100", dstAddr: "142.250.74.110", srcPort: "54321", dstPort: "443", inIface: "lan", outIface: "wan", connState: "new", srcAddrList: "", dstAddrList: "", tlsHost: "" } + const rules = Array.from({ length: 1000 }, (_, i) => ({ id: `s${i + 1}`, chain: "forward", action: i === 999 ? "drop" : "passthrough", proto: "all", src: "", dst: "", port: "", iface: "", comment: `scenario-${i + 1}`, enabled: true })) + const scenarios = [{ id: "big", name: "Сценарий1000", description: "", packet, rules, createdAt: 1 }] + await page.addInitScript(value => localStorage.setItem("routerlists-fw-scenarios", JSON.stringify(value)), scenarios) + let writes = 0 + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + if (r.request().method() !== "GET") writes++ + const p = new URL(r.request().url()).pathname + return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/firewall/all" ? { rules: [], addressLists: [] } : p === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", type: "exit-node", enabled: true, country: "RU", status: "online" }] : {} }) + }) + await page.goto("/firewall?server=1") + await page.getByRole("button", { name: "Симулятор", exact: true }).click() + await page.getByRole("button", { name: "Редактировать сценарий Сценарий1000", exact: true }).click() + const dialog = page.getByRole("dialog"), rows = dialog.locator("tr[data-row-id]") + await expect(rows).toHaveCount(50) + console.log("Scenario profile", JSON.stringify({ count: 1000, rows: await rows.count(), modalNodes: await dialog.locator("*").count() })) + await expect(dialog.getByRole("button", { name: "Поднять правило 1", exact: true })).toBeDisabled() + await dialog.getByRole("button", { name: "Опустить правило 50", exact: true }).click() + await expect(rows.last()).toHaveAttribute("data-row-id", "s51") + await dialog.getByRole("button", { name: "Следующая страница", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "s50") + await dialog.getByRole("button", { name: "Поднять правило 51", exact: true }).focus(); await page.keyboard.press("Enter") + await expect(rows.first()).toHaveAttribute("data-row-id", "s51") + await dialog.getByRole("button", { name: "Удалить правило 51", exact: true }).click() + await expect(rows.first()).toHaveAttribute("data-row-id", "s52") + await page.setViewportSize({ width: 320, height: 1000 }) + const remove = rows.first().getByRole("button", { name: "Удалить правило 51", exact: true }) + await remove.scrollIntoViewIfNeeded() + await dialog.locator('[data-slot="scroll-area-viewport"]').evaluate(el => { el.scrollLeft = el.scrollWidth }) + await expect(remove).toBeInViewport(); const box = (await remove.boundingBox())! + expect(box.height).toBeGreaterThanOrEqual(44); expect(box.width).toBeGreaterThanOrEqual(44); expect(box.x + box.width).toBeLessThanOrEqual(320) + expect((await dialog.boundingBox())?.width).toBe(320) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320) + await page.addScriptTag({ content: axe.source }) + expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[role="dialog"] [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([]) + await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) + await page.screenshot({ path: info.outputPath("scenario-320.png"), animations: "disabled" }) + await dialog.getByRole("button", { name: "Сохранить изменения", exact: true }).click() + await expect(dialog).toHaveCount(0) + const saved = await page.evaluate(() => JSON.parse(localStorage.getItem("routerlists-fw-scenarios")!)) + expect(saved[0].rules).toHaveLength(999) + expect(saved[0].rules.slice(48,52).map((r: { id: string }) => r.id)).toEqual(["s49", "s50", "s52", "s53"]) + expect(saved[0].rules[998]).toEqual(rules[999]) + await expect(page.getByRole("button", { name: "Активен", exact: true })).toBeVisible() + await page.getByRole("button", { name: "Мгновенно", exact: true }).click() + await page.getByRole("button", { name: "Запустить", exact: true }).click() + await expect(page.getByText("— правило #999", { exact: true })).toBeVisible() + await expect(page.getByText("Пакет заблокирован", { exact: false })).toBeVisible() + console.log("Scenario simulation nodes", await page.locator("*").count()) + expect(writes).toBe(0) +})