diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index 9c3798ae..1f744353 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -323,34 +323,6 @@ const CHAIN_GROUPS: { { id: "simulator", label: "Симулятор", icon: , chains: [] }, ] -// ─── Action styles ──────────────────────────────────────────────────────────── - -const ACTION_STYLES: Record = { - accept: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", - drop: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - reject: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - masquerade: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", - "mark-routing": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", - "mark-conn": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", - "fasttrack-connection": "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20", - "dst-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20", - "src-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20", - "add-src-to-address-list": "bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20", -} - -const CHAIN_STYLES: Record = { - forward: "bg-foreground/5 text-foreground/70", - input: "bg-violet-500/10 text-violet-600 dark:text-violet-400", - output: "bg-sky-500/10 text-sky-600 dark:text-sky-400", - srcnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400", - dstnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400", - prerouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400", - postrouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400", - "ip6-input": "bg-violet-500/10 text-violet-600 dark:text-violet-400", - "ip6-forward": "bg-foreground/5 text-foreground/70", - "ip6-output": "bg-sky-500/10 text-sky-600 dark:text-sky-400", -} - // ─── Default form ───────────────────────────────────────────────────────────── const defaultForm = { @@ -488,7 +460,7 @@ function generateRsc(rules: FirewallRule[], timestamp?: string): string { // ─── Rule Sheet ─────────────────────────────────────────────────────────────── -function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, saving = false, error }: { +function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, saving = false, error, returnFocus }: { open: boolean onClose: () => void initialRule: Partial | null @@ -497,27 +469,32 @@ function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, sav onSave: (payload: RuleWritePayload) => void saving?: boolean error?: string | null + returnFocus?: () => HTMLElement | null }) { const isNew = !initialRule?.id const [form, setForm] = useState(defaultForm) + const [formContext, setFormContext] = useState({ open: false, initialRule, chainGroup }) - useEffect(() => { - if (!open) return - setForm({ - ...defaultForm, - chain: initialRule?.chain ?? (CHAIN_GROUPS.find(g => g.id === chainGroup)?.chains[0] ?? "forward"), - action: (initialRule?.action as string) ?? defaultForm.action, - proto: initialRule?.proto ?? defaultForm.proto, - comment: initialRule?.comment ?? "", - enabled: initialRule?.enabled ?? true, - srcAddrList: initialRule?.src && initialRule.src !== "—" ? initialRule.src : "", - dstAddrList: initialRule?.dst && initialRule.dst !== "—" ? initialRule.dst : "", - dstPort: initialRule?.port && initialRule.port !== "—" ? initialRule.port : "", - inIface: initialRule?.iface && initialRule.iface !== "—" ? initialRule.iface : "", - log: initialRule?.log ?? false, - logPrefix: initialRule?.logPrefix ?? "", - }) - }, [open, initialRule, chainGroup]) + // Adjust this component before committing children with a stale draft. + if (formContext.open !== open || formContext.initialRule !== initialRule || formContext.chainGroup !== chainGroup) { + setFormContext({ open, initialRule, chainGroup }) + if (open) { + setForm({ + ...defaultForm, + chain: initialRule?.chain ?? (CHAIN_GROUPS.find(g => g.id === chainGroup)?.chains[0] ?? "forward"), + action: (initialRule?.action as string) ?? defaultForm.action, + proto: initialRule?.proto ?? defaultForm.proto, + comment: initialRule?.comment ?? "", + enabled: initialRule?.enabled ?? true, + srcAddrList: initialRule?.src && initialRule.src !== "—" ? initialRule.src : "", + dstAddrList: initialRule?.dst && initialRule.dst !== "—" ? initialRule.dst : "", + dstPort: initialRule?.port && initialRule.port !== "—" ? initialRule.port : "", + inIface: initialRule?.iface && initialRule.iface !== "—" ? initialRule.iface : "", + log: initialRule?.log ?? false, + logPrefix: initialRule?.logPrefix ?? "", + }) + } + } const set = (k: K, v: RuleForm[K]) => setForm((f) => ({ ...f, [k]: v })) @@ -534,7 +511,7 @@ function RuleSheet({ open, onClose, initialRule, chainGroup, family, onSave, sav return ( { if (!v && !saving) onClose() }}> - + {isNew ? "Новое правило" : "Редактировать правило"} @@ -750,9 +727,7 @@ function AddressListsTab({ return ["all", ...Array.from(s)] }, [entries]) - useEffect(() => { - if (listFilter !== "all" && !lists.includes(listFilter)) setListFilter("all") - }, [lists, listFilter]) + if (listFilter !== "all" && !lists.includes(listFilter)) setListFilter("all") const filtered = useMemo(() => { return entries.filter((e) => { @@ -793,7 +768,7 @@ function AddressListsTab({ ))} {filtered.length} записей - +
@@ -812,7 +787,7 @@ function AddressListsTab({
{listEntries.map((e) => ( -
@@ -865,7 +840,7 @@ const defaultAddrForm = { } function AddressListSheet({ - open, onClose, initial, family, onSave, saving = false, error, + open, onClose, initial, family, onSave, saving = false, error, returnFocus, }: { open: boolean onClose: () => void @@ -874,24 +849,28 @@ function AddressListSheet({ onSave: (payload: { list: string; address: string; comment?: string; timeout?: string; disabled?: boolean }) => void saving?: boolean error?: string | null + returnFocus?: () => HTMLElement | null }) { const isNew = !initial?.id const [form, setForm] = useState(defaultAddrForm) + const [formContext, setFormContext] = useState({ open: false, initial }) - useEffect(() => { - if (!open) return - setForm({ - list: initial?.list ?? "", - address: initial?.address ?? "", - comment: initial?.comment ?? "", - timeout: initial?.timeout ?? "", - enabled: !(initial?.disabled ?? false), - }) - }, [open, initial]) + if (formContext.open !== open || formContext.initial !== initial) { + setFormContext({ open, initial }) + if (open) { + setForm({ + list: initial?.list ?? "", + address: initial?.address ?? "", + comment: initial?.comment ?? "", + timeout: initial?.timeout ?? "", + enabled: !(initial?.disabled ?? false), + }) + } + } return ( { if (!v && !saving) onClose() }}> - + {isNew ? "Запись address-list" : "Редактировать запись"} @@ -1986,7 +1965,7 @@ function FirewallPageInner() { const [deleteError, setDeleteError] = useState(null) const [selectedServerId, setSelectedServerId] = useState(serverFromQuery ?? ALL_SERVERS_ID) - const appliedQueryRef = useRef(false) + const [queryApplied, setQueryApplied] = useState(false) const [chainGroup, setChainGroup] = useState("filter") const [chainFilter, setChainFilter] = useState("all") @@ -2109,14 +2088,10 @@ function FirewallPageInner() { const displayAddrLists = isLive ? liveAddrLists : mockAddrLists const displayServers = isLive ? liveServers : mockServers - useEffect(() => { - if (appliedQueryRef.current || !serverFromQuery) return - if (displayServers.length === 0) return - if (displayServers.some((s) => s.id === serverFromQuery)) { - setSelectedServerId(serverFromQuery) - } - appliedQueryRef.current = true - }, [displayServers, serverFromQuery]) + if (!queryApplied && serverFromQuery && displayServers.length > 0) { + if (displayServers.some(server => server.id === serverFromQuery)) setSelectedServerId(serverFromQuery) + setQueryApplied(true) + } const scopedRules = useMemo(() => { if (selectedServerId === ALL_SERVERS_ID) return displayRules @@ -2585,7 +2560,7 @@ function FirewallPageInner() { - @@ -2805,6 +2780,7 @@ function FirewallPageInner() { { if (!mutationLock.current) setRuleSheetOpen(false) }} + returnFocus={() => (editingRule?.id ? document.querySelector(`tr[data-row-id="${CSS.escape(editingRule.id)}"] button[aria-haspopup="menu"]`) : null) ?? document.querySelector("[data-firewall-new-rule]")} initialRule={editingRule} chainGroup={sheetChainGroup} family={editingRule?.id ? ruleFamily(editingRule as FirewallRule) : ipFamily} @@ -2816,6 +2792,7 @@ function FirewallPageInner() { { if (!mutationLock.current) setAddrSheetOpen(false) }} + returnFocus={() => (editingAddr?.id ? document.querySelector(`[data-firewall-address-id="${CSS.escape(editingAddr.id)}"] button[aria-haspopup="menu"]`) : null) ?? document.querySelector("[data-firewall-add-address]")} initial={editingAddr} family={editingAddr?.id ? addressFamily(editingAddr as AddressListEntry) : ipFamily} onSave={(payload) => { void saveAddr(payload) }} diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 78f2709a..6acbf9d6 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -89,3 +89,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 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 остаются. 12.3.46: trace999/15529→1293 page nodes,50 steps/whole verdict#999/страницы/jump/stop→restart epoch/reset/320px/axe проверены;6+1 browser/type/test lint/build прошли. Полный steps в памяти и O(N²) обновления instant не менялись; общий CPU performance не принят.10 manualPagination:true остаются. + +12.3.47: Firewall draft/URL/list filter/focus/lint cleanup завершены,26 main/context4/type/полный ESLint страницы/build прошли.10 manualPagination:true остаются; далее полный OSPF1000 baseline, включая SVG topology. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index b17da40c..ac6f6c78 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 lint cleanup; затем 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/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: OSPF1000/таблица+topology baseline, затем ограничения DOM/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 1ea4892c..dd4b458a 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.47 — формы, URL и lint Firewall (2026-10-04) + +RuleSheet/AddressListSheet корректируют собственный draft при смене контекста до commit дочерних элементов, сохраняя ввод при API ошибке и pending. Sheet не перемонтируется; finalFocus возвращает к исходной кнопке меню строки, fallback — create/add. Address-list фильтр отсутствующего списка сбрасывается до вывода детей и не возвращается при повторном появлении списка. Query server применяется один раз после каталога через guarded state adjustment; начальный useState недостаточен для асинхронного URL-контекста. Удалены дубли unused styles. + +6+20 main browser и4/4 context прошли: reopen старого rule/address после несохранённого draft, create с пустым draft, Escape/focus, удаление/возврат фильтра,8 save cases/ошибки API/права/partial/cache/stale, обе большие таблицы/trace и поздние ответы. TypeScript без incremental/ESLint страницы и нового теста/production build прошли. Четыре исходные lint errors/2 warnings устранены без suppression; подход соответствует [React: Adjusting some state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes). Сетевые правила не изменены, real RouterOS не проверен. + +Следующий блок12.3.48: полный профиль OSPF1000 соседей/1001 SVG узел, таблица и topology вместе; определить точную границу DOM до изменений. Затем ограничение таблицы/графа, optimizer. LocalStorage отказ сценария и большой multi-server sort Firewall остаются отдельными проверками.10 manualPagination:true остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.46 — ограниченная трассировка Firewall (2026-10-04) Trace показывает50 шагов, общий verdict/counts/номера относятся ко всему расчёту. Предыдущая/следующая страница и переход к правилу результата44px; новая симуляция/reset/preset/scenario начинают с первой страницы. Строки переносятся на320px, исправлен контраст passthrough и пояснений. runEpoch защищает от старого таймера после stop→новый run; unmount инвалидирует запуск. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 8f115bc0..e1a247e5 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -569,3 +569,12 @@ Trace показывает50 шагов, общий verdict/counts/номера Следующий блок12.3.47: Firewall lint cleanup четырёх старых эффектов без потери draft/URL/filter/focus. Затем OSPF таблица+topology/optimizer; ошибки localStorage и большой multi-server sort отдельно.10 manualPagination:true остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + +## 12.3.47 — формы, URL и lint Firewall (2026-10-04) + +RuleSheet/AddressListSheet корректируют собственный draft при смене контекста до commit дочерних элементов, сохраняя ввод при API ошибке и pending. Sheet не перемонтируется; finalFocus возвращает к исходной кнопке меню строки, fallback — create/add. Address-list фильтр отсутствующего списка сбрасывается до вывода детей и не возвращается при повторном появлении списка. Query server применяется один раз после каталога через guarded state adjustment; начальный useState недостаточен для асинхронного URL-контекста. Удалены дубли unused styles. + +6+20 main browser и4/4 context прошли: reopen старого rule/address после несохранённого draft, create с пустым draft, Escape/focus, удаление/возврат фильтра,8 save cases/ошибки API/права/partial/cache/stale, обе большие таблицы/trace и поздние ответы. TypeScript без incremental/ESLint страницы и нового теста/production build прошли. Четыре исходные lint errors/2 warnings устранены без suppression; подход соответствует [React: Adjusting some state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes). Сетевые правила не изменены, real RouterOS не проверен. + +Следующий блок12.3.48: полный профиль OSPF1000 соседей/1001 SVG узел, таблица и topology вместе; определить точную границу DOM до изменений. Затем ограничение таблицы/графа, optimizer. LocalStorage отказ сценария и большой multi-server sort Firewall остаются отдельными проверками.10 manualPagination:true остаются. Общая main/context после delta и внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/tests/e2e/firewall-draft-reset.spec.ts b/tests/e2e/firewall-draft-reset.spec.ts new file mode 100644 index 00000000..324d00dd --- /dev/null +++ b/tests/e2e/firewall-draft-reset.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from "@playwright/test" + +for (const kind of ["rule", "address"] as const) { + test(`Firewall: новый draft ${kind} при повторном открытии и возврат фокуса`, async ({ page }) => { + const rule = { id: "r1", rosId: "*1", serverId: "1", serverName: "Роутер", family: "ip", table: "filter", chain: "forward", action: "accept", proto: "tcp", src: "", dst: "", port: "443", iface: "", comment: "Исходный комментарий", enabled: true, hits: 1 } + const entry = { id: "a1", rosId: "*A1", serverId: "1", serverName: "Роутер", family: "ip", list: "trusted", address: "192.0.2.10", comment: "Исходный комментарий", disabled: false } + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const p = new URL(r.request().url()).pathname + return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/firewall/all" ? { rules: [rule], addressLists: [entry] } : 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 expect(page.locator("tr[data-row-id]")).toHaveCount(1) + if (kind === "address") await page.getByRole("button", { name: /Адр. листы/ }).click() + const action = page.getByRole("button", { name: kind === "rule" ? "Действия: forward accept" : "Действия: 192.0.2.10", exact: true }) + const open = async () => { await action.click(); await page.getByRole("menuitem", { name: "Редактировать", exact: true }).click() } + await open() + const dialog = page.getByRole("dialog") + const comment = dialog.locator('[data-slot="field"]').filter({ has: page.getByText("Комментарий", { exact: true }) }).getByRole("textbox") + await expect(comment).toHaveValue("Исходный комментарий") + await comment.fill("Несохранённый draft"); await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0); await expect(action).toBeFocused() + await open(); await expect(comment).toHaveValue("Исходный комментарий") + await dialog.getByRole("button", { name: "Отмена", exact: true }).click(); await expect(dialog).toHaveCount(0) + await page.getByRole("button", { name: kind === "rule" ? "Новое правило" : "Добавить", exact: true }).click() + await expect(comment).toHaveValue("") + }) +} + +test("Firewall: удалённый list фильтр сбрасывается и не возвращается после refresh", async ({ page }) => { + const entry = { id: "a1", rosId: "*A1", serverId: "1", serverName: "Роутер", family: "ip", list: "trusted", address: "192.0.2.10", comment: "", disabled: false } + let entries = [entry] + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", r => { + const p = new URL(r.request().url()).pathname + return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : p === "/api/firewall/all" ? { rules: [], addressLists: entries } : 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: /Адр. листы/ }).click() + await page.getByRole("button", { name: "trusted", exact: true }).click() + const other = { ...entry, id: "a2", rosId: "*A2", list: "other", address: "192.0.2.20" } + entries = [other]; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.getByRole("button", { name: "Действия: 192.0.2.20", exact: true })).toBeVisible() + entries = [entry, other]; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.getByRole("button", { name: "Действия: 192.0.2.10", exact: true })).toBeVisible() + await expect(page.getByRole("button", { name: "Действия: 192.0.2.20", exact: true })).toBeVisible() +})