diff --git a/app/(main)/recursive-routes/page.tsx b/app/(main)/recursive-routes/page.tsx index 378940aa..9b0ff2d4 100644 --- a/app/(main)/recursive-routes/page.tsx +++ b/app/(main)/recursive-routes/page.tsx @@ -8,6 +8,7 @@ import { RecursiveRoutesDataGrid, type RecursiveRouteGroup, } from "@/components/data-grids/recursive-routes-data-grid" +import { RecursiveGatewayChoices, type GatewayOption } from "@/components/recursive-gateway-choices" import { FormField, SectionTitle } from "@/components/form-kit" import { DataPageCard } from "@/components/data-page-card" import { Frame, FramePanel } from "@/components/reui/frame" @@ -42,13 +43,6 @@ interface BackendServer { latency: number | null } -interface GatewayOption { - id: string - name: string - ip: string - status: "active" | "inactive" | "unknown" -} - const COUNTRY_OPTIONS = [ { code: "RU", label: "Россия" }, { code: "DE", label: "Германия" }, { code: "NL", label: "Нидерланды" }, { code: "SG", label: "Сингапур" }, @@ -195,10 +189,15 @@ function RouteSheet({ returnFocus: () => HTMLElement | null }) { const [form, setForm] = useState(initial) + const [endpointPage, setEndpointPage] = useState(0) + const lastEndpointPage = Math.max(0, Math.ceil(form.endpoints.length / 10) - 1) + if (endpointPage > lastEndpointPage) setEndpointPage(lastEndpointPage) + const currentEndpointPage = Math.min(endpointPage, lastEndpointPage) + const endpointOffset = currentEndpointPage * 10 const [error, setError] = useState(null) // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { setForm(initial); setError(null) }, [initial, open]) + useEffect(() => { setForm(initial); setError(null); setEndpointPage(0) }, [initial, open]) const set = (k: K, v: RouteForm[K]) => setForm(f => ({ ...f, [k]: v })) const setEp = (id: string, k: K, v: RouteEndpointForm[K]) => setForm(f => ({ ...f, endpoints: f.endpoints.map(ep => ep.id === id ? { ...ep, [k]: v } : ep) })) @@ -208,9 +207,11 @@ function RouteSheet({ if (busy || saveLock.current) return if (!form.dstAddress.trim()) { setError("Dst Address обязателен"); return } if (form.endpoints.length === 0) { setError("Добавь хотя бы одну конечную точку"); return } - if (form.endpoints.some(ep => !ep.gateway.trim())) { setError("У каждой конечной точки должен быть Gateway"); return } + const emptyGateway = form.endpoints.findIndex(ep => !ep.gateway.trim()) + if (emptyGateway >= 0) { setEndpointPage(Math.floor(emptyGateway / 10)); setError(`У каждой конечной точки должен быть Gateway (Endpoint ${emptyGateway + 1})`); return } if (!form.routingTable.trim()) { setError("Укажите таблицу маршрутизации"); return } - if (form.endpoints.some(ep => [ep.distance, ep.scope, ep.targetScope].some(value => value !== null && (!Number.isInteger(value) || value < 0 || value > 255)))) { setError("Distance, Scope и T.Scope должны быть целыми числами от 0 до 255"); return } + const invalidEndpoint = form.endpoints.findIndex(ep => [ep.distance, ep.scope, ep.targetScope].some(value => value !== null && (!Number.isInteger(value) || value < 0 || value > 255))) + if (invalidEndpoint >= 0) { setEndpointPage(Math.floor(invalidEndpoint / 10)); setError(`Distance, Scope и T.Scope должны быть целыми числами от 0 до 255 (Endpoint ${invalidEndpoint + 1})`); return } setError(null) saveLock.current = true try { await onSave(form) } finally { saveLock.current = false } @@ -238,18 +239,31 @@ function RouteSheet({
Конечные точки + {form.endpoints.length > 10 &&
+ { + if (event.key !== "Enter") return + event.preventDefault() + const number = Number(event.currentTarget.value) + if (Number.isInteger(number) && number >= 1 && number <= form.endpoints.length) { setEndpointPage(Math.floor((number - 1) / 10)); setError(null) } + else setError("Укажите номер endpoint от 1 до " + form.endpoints.length) + }} /> + Номер endpoint → Enter + Endpoints {endpointOffset + 1}–{Math.min(endpointOffset + 10, form.endpoints.length)} из {form.endpoints.length} + + +
}
- {form.endpoints.map((ep, idx) => ( -
+ {form.endpoints.slice(endpointOffset, endpointOffset + 10).map((ep, idx) => ( +
- Endpoint {idx + 1}{ep.disabled ? " · отключён" : ""} + Endpoint {endpointOffset + idx + 1}{ep.disabled ? " · отключён" : ""}
-
- {gateways.map((g) => { - const value = `${g.ip}%${g.name}` - const selected = ep.gateway === value - return ( - - ) - })} -
+ setEp(ep.id, "gateway", value)} />
))} - {gateways.length === 0 && ( diff --git a/components/recursive-gateway-choices.tsx b/components/recursive-gateway-choices.tsx new file mode 100644 index 00000000..954e2990 --- /dev/null +++ b/components/recursive-gateway-choices.tsx @@ -0,0 +1,50 @@ +"use client" +import { useMemo, useState } from "react" +import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" +import { CheckIcon } from "lucide-react" +export interface GatewayOption { id: string; name: string; ip: string; status: "active" | "inactive" | "unknown" } +export function RecursiveGatewayChoices({ gateways, selectedValue, endpointNumber, onChange }: { gateways: GatewayOption[]; selectedValue: string; endpointNumber: number; onChange: (value: string) => void }) { + const [search, setSearch] = useState("") + const matches = useMemo(() => gateways.filter(g => (g.ip + "%" + g.name).toLowerCase().includes(search.toLowerCase())), [gateways, search]) + const selected = matches.find(g => g.ip + "%" + g.name === selectedValue) + const visible = (selected ? [selected, ...matches.filter(g => g.id !== selected.id)] : matches).slice(0, 5) + return
+ {gateways.length > 5 && setSearch(event.target.value)} />} + {gateways.length > 5 &&

Показано {visible.length} из {matches.length} шлюзов. Поиск охватывает весь список.

} + {search && matches.length === 0 &&

Шлюзы не найдены. Измените поиск или введите адрес вручную.

} + {visible.map((g) => { + const value = `${g.ip}%${g.name}` + const selected = selectedValue === value + return ( + + ) + })} +
+} diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 6f97bd39..e46eb5d9 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -33,7 +33,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 и связывание с графиками. | -Очередь: форма recursive routes/gateway choices → Filters порядок → Firewall/DnD → OSPF таблица+topology → optimizer preview/комбинаторика. Scheduler11, traffic cap50 и commRecs24 проверять на реальной границе, не искусственно подставлять1000 в недоступный DTO. Demo и неиспользуемый Subusers отделены от live приёмки. Все новые профили должны сохранять search/sort/refresh/clamp, точный target и права,320px и URL scope. +Очередь: Filters порядок → Firewall/DnD → 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). @@ -80,3 +80,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.38: Communities1000/190573 JSON bytes →50 DOM rows/2216 page nodes/1206ms dev ready.19 browser/type/lint/build прошли.14 manualPagination:true остаются; обновления/selection по ID подтверждены локально. 12.3.40: recursive1000 групп→50 rows/2118 nodes/1457ms; одна группа1000 endpoints→50 cards/1126 nodes.35 browser/type/lint/build прошли; большой RouteSheet пока вне принятого performance scope.13 manualPagination:true остаются. + +12.3.41: recursive RouteSheet1000 endpoints ×1000 gateways →10 cards/1569 modal nodes, максимум5 gateway choices на карточку; полный PUT1001 сохранён.22+1 browser/type/lint/build. Расчёт/валидация полного draft остаётся на клиенте. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index b40b0ef9..99e0717a 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: большая форма маршрута/шлюзы и полная 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: Filters pagination/порядок и полная 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 615bf3b0..8ad94727 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.41 — большая форма рекурсивного маршрута (2026-10-04) + +RouteSheet показывает10 endpoints, но хранит весь draft. Предыдущая/следующая страница и прямой переход по номеру endpoint доступны с клавиатуры; add открывает страницу нового элемента, удаление корректирует индекс. Валидация ищет ошибки во всём draft и открывает страницу первого невалидного endpoint. RecursiveGatewayChoices сохраняет прежние карточки/семантику статуса; показывает5 вариантов (выбранный первым) и ищет по всему каталогу IP/name. Ручной gateway ввод сохранён. + +22 профильных browser прошли; финальный large повтор1/1 после прямого перехода/скрытой ошибки.1000 endpoints ×1000 gateways →10 cards/1569 modal nodes,50 gateway choices. Проверены первая/последняя страницы, поиск шлюза1000, скрытый невалидный endpoint1000 без PUT, ошибка503/сохранение draft/повтор,320px; PUT содержит1001 строки, обе скрытые правки и неизменную другую группу. TypeScript без incremental/финальный ESLint и production frontend build прошли. [Форма320px](ux-ui/screens/large-recursive-form-320.png) просмотрена. Общие сетевые правила и права не изменены; реальный RouterOS PUT не проверен. + +Следующий блок12.3.42: Filters1000/порядок через границу страниц/whole-catalog PUT/search/sort/refresh/cache/320px.13 manualPagination:true specialized grids остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.40 — большие группы рекурсивных маршрутов (2026-10-04) ReUI pagination50 групп, global sort, refresh без сброса страницы и clamp. Первичное/новое URL раскрытие находит группу в sorted model и открывает её страницу; обычный переход страниц не навязывает возврат к выбранной группе. Строка показывает3 gateway и точное число остальных. Раскрытие имеет отдельные страницы50 endpoints, сортирует всю группу по distance, сохраняет страницу при refresh и корректирует её при сокращении. На320px route закреплён, edit/delete44px и доступны через горизонтальный scroll; детали ограничены viewport и доступны с клавиатуры. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index d8df9787..a78a7c2f 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -515,3 +515,12 @@ ReUI pagination50 групп, global sort, refresh без сброса стра Осталось13 manualPagination:true specialized grids. Большая RouteSheet пока рендерит все endpoints и все варианты gateways для каждого endpoint; performance формы не принят. Следующий блок12.3.41: ограничить DOM формы/вариантов шлюзов без потери скрытых данных/валидации/PUT payload, проверить1000 endpoints и1000 gateways. Затем Filters порядок/Firewall DnD и остальные этапы карты. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + +## 12.3.41 — большая форма рекурсивного маршрута (2026-10-04) + +RouteSheet показывает10 endpoints, но хранит весь draft. Предыдущая/следующая страница и прямой переход по номеру endpoint доступны с клавиатуры; add открывает страницу нового элемента, удаление корректирует индекс. Валидация ищет ошибки во всём draft и открывает страницу первого невалидного endpoint. RecursiveGatewayChoices сохраняет прежние карточки/семантику статуса; показывает5 вариантов (выбранный первым) и ищет по всему каталогу IP/name. Ручной gateway ввод сохранён. + +22 профильных browser прошли; финальный large повтор1/1 после прямого перехода/скрытой ошибки.1000 endpoints ×1000 gateways →10 cards/1569 modal nodes,50 gateway choices. Проверены первая/последняя страницы, поиск шлюза1000, скрытый невалидный endpoint1000 без PUT, ошибка503/сохранение draft/повтор,320px; PUT содержит1001 строки, обе скрытые правки и неизменную другую группу. TypeScript без incremental/финальный ESLint и production frontend build прошли. [Форма320px](ux-ui/screens/large-recursive-form-320.png) просмотрена. Общие сетевые правила и права не изменены; реальный RouterOS PUT не проверен. + +Следующий блок12.3.42: Filters1000/порядок через границу страниц/whole-catalog PUT/search/sort/refresh/cache/320px.13 manualPagination:true specialized grids остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/docs/ux-ui/screens/large-recursive-form-320.png b/docs/ux-ui/screens/large-recursive-form-320.png new file mode 100644 index 00000000..1da3cccf Binary files /dev/null and b/docs/ux-ui/screens/large-recursive-form-320.png differ diff --git a/tests/e2e/large-recursive-form.spec.ts b/tests/e2e/large-recursive-form.spec.ts new file mode 100644 index 00000000..0f3ef27e --- /dev/null +++ b/tests/e2e/large-recursive-form.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test" +test("Recursive form1000 endpoints/gateways: full draft/payload/error/retry/320px", async ({ page }, info) => { + test.setTimeout(150000) + let routes = Array.from({length:1000},(_,i)=>({id:"r"+(i+1),dstAddress:"203.0.113.0/24",gateway:"192.0.2.1%gw-"+(i+1),distance:1,scope:30,targetScope:10,routingTable:"main",checkGateway:"ping",comment:"Большая группа",disabled:false,country:""})) + const other = {...routes[0],id:"other",dstAddress:"203.0.114.0/24",gateway:"198.51.100.1%other",routingTable:"other"};routes.push(other) + const gateways=Array.from({length:1000},(_,i)=>({id:"g"+(i+1),name:"gw-"+(i+1),ip:"192.0.2.1",status:"unknown"})) + let fail=true,attempts=0,submitted:typeof routes=[] + await page.route("**/health",r=>r.fulfill({json:{ok:true}})) + await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname + if(p==="/api/recursive-routes"&&r.request().method()==="PUT"){attempts++;submitted=r.request().postDataJSON().routes;if(fail)return r.fulfill({status:503,json:{error:"Запись недоступна тест"}});routes=submitted;return r.fulfill({json:{ok:true,serverId:1,routes,pushedRoutes:routes.length}})} + return r.fulfill({json:p==="/api/auth/config"?{required:false}:p==="/api/servers"?[{id:1,name:"Роутер",host:"192.0.2.1",country:"RU",enabled:true,status:null}]:p==="/api/recursive-routes/catalog"?{serverId:1,routes,live:true,stale:false}:p==="/api/recursive-routes/gateways/catalog"?{serverId:1,gateways}:{} }) + }) + await page.goto("/recursive-routes?server=1") + await page.getByRole("button",{name:"Редактировать маршрут 203.0.113.0/24 · main",exact:true}).click() + const sheet=page.getByRole("dialog"),cards=sheet.locator('[data-recursive-endpoint-form]') + await expect(cards).toHaveCount(10);await expect(sheet.getByRole("button",{name:/^Выбрать шлюз /})).toHaveCount(50) + const profile={endpoints:1000,gateways:1000,cards:await cards.count(),nodes:await sheet.locator("*").count()};console.log("Recursive form profile",JSON.stringify(profile));await info.attach("profile",{body:JSON.stringify(profile),contentType:"application/json"}) + await cards.first().getByRole("textbox",{name:"Шлюз",exact:true}).fill("192.0.2.10%edited-first") + const next=sheet.getByRole("button",{name:"Следующие endpoints формы",exact:true});await next.click();await expect(cards.first()).toContainText("Endpoint 11");const jump=sheet.getByRole("spinbutton",{name:"Перейти к endpoint",exact:true});await jump.fill("1000");await jump.press("Enter") + await expect(next).toBeDisabled();await expect(cards.last()).toContainText("Endpoint 1000") + await cards.first().getByRole("textbox",{name:"Поиск шлюза для endpoint 991",exact:true}).fill("gw-1000") + await cards.first().getByRole("button",{name:"Выбрать шлюз 192.0.2.1%gw-1000",exact:true}).click() + await expect(cards.first().getByRole("textbox",{name:"Шлюз",exact:true})).toHaveValue("192.0.2.1%gw-1000") + await cards.last().getByRole("textbox",{name:"Шлюз",exact:true}).fill("") + await sheet.getByRole("button",{name:"Предыдущие endpoints формы",exact:true}).click();await expect(cards.first()).toContainText("Endpoint 981");const save=sheet.getByRole("button",{name:"Сохранить",exact:true});await save.click();await expect(sheet.getByRole("alert")).toContainText("Endpoint 1000");expect(attempts).toBe(0) + await cards.last().getByRole("textbox",{name:"Шлюз",exact:true}).fill("192.0.2.99%edited-last") + await save.click();await expect(sheet.getByRole("alert")).toContainText("Запись недоступна тест");expect(submitted).toHaveLength(1001) + expect(submitted.find(r=>r.gateway==="192.0.2.10%edited-first")).toBeTruthy();expect(submitted.find(r=>r.gateway==="192.0.2.99%edited-last")).toBeTruthy();expect(submitted.find(r=>r.id==="other")).toEqual(other) + await expect(cards.last().getByRole("textbox",{name:"Шлюз",exact:true})).toHaveValue("192.0.2.99%edited-last") + await page.setViewportSize({width:320,height:1000});await cards.first().scrollIntoViewIfNeeded();expect(await sheet.evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true) + await page.addStyleTag({content:"nextjs-portal{display:none!important}"});await page.screenshot({path:info.outputPath("recursive-form-320.png"),animations:"disabled"}) + fail=false;await save.click();await expect(sheet).toHaveCount(0);expect(attempts).toBe(2);expect(submitted).toHaveLength(1001) +})