From 2e648dcf1c9138562f987b339ce06b10c9cd445f Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 16:29:31 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20Firewall=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D1=87=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/firewall/page.tsx | 43 +++++++++++++++++++------------- docs/ux-ui-performance.md | 2 ++ docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 ++++++ docs/ux-ui-stage-12.md | 9 +++++++ tests/e2e/firewall-stale.spec.ts | 43 ++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 tests/e2e/firewall-stale.spec.ts diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index 8d479d6e..be356f6b 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -1936,6 +1936,7 @@ function FirewallPageInner() { const [dataLoading, setDataLoading] = useState(false) const [dataError, setDataError] = useState(null) const [hasLoaded, setHasLoaded] = useState(false) + const [readTrusted, setReadTrusted] = useState(false) const [sliceTimes, setSliceTimes] = useState([]) const [failures, setFailures] = useState([]) const loadEpoch = useRef(0) @@ -1995,8 +1996,10 @@ function FirewallPageInner() { const receivedAt = new Date().toISOString() setSliceTimes(previous => updateFirewallSliceTimes(previous, [...enabledIds], firewall.data.failures, receivedAt)) setHasLoaded(true) + setReadTrusted(true) } catch (e) { if (epoch !== loadEpoch.current) return + setReadTrusted(false) setDataError(e instanceof Error ? e.message : "Ошибка загрузки") } finally { if (epoch === loadEpoch.current) setDataLoading(false) @@ -2029,6 +2032,7 @@ function FirewallPageInner() { const restoreRevision = useCallback(async (id: string) => { if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live") if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall") + if (!readTrusted) throw new Error("Обновите актуальные данные Firewall перед восстановлением") if (failures.some(f => f.serverId === historyServerId)) throw new Error("Обновите полный набор данных устройства перед восстановлением") mutationLock.current = true loadEpoch.current += 1 @@ -2050,7 +2054,7 @@ function FirewallPageInner() { mutationLock.current = false setHistoryRestoring(false) } - }, [isLive, historyServerId, apiFetch, loadLive, loadRevisions, failures]) + }, [isLive, historyServerId, apiFetch, loadLive, loadRevisions, failures, readTrusted]) useEffect(() => { queueMicrotask(() => { @@ -2156,6 +2160,7 @@ function FirewallPageInner() { const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : [] const firewallIncomplete = scopedFailures.length > 0 + const firewallWriteBlocked = firewallIncomplete || (isLive && !readTrusted) const scopedSliceTimes = sliceTimes.filter(slice => (selectedServerId === ALL_SERVERS_ID || slice.serverId === selectedServerId) && slice.family === ipFamily) const freshness = firewallTimeRange(scopedSliceTimes) const totalEnabled = familyRules.filter((r) => r.enabled).length @@ -2171,7 +2176,7 @@ function FirewallPageInner() { async function toggleRule(id: string) { const r = displayRules.find((x) => x.id === id) - if (!r || mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (!r || mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return if (isLive) { const key = ruleKey(r) if (!key) return @@ -2202,13 +2207,13 @@ function FirewallPageInner() { } function requestDelete(target: NonNullable) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return setDeleteError(null) setPendingDelete(target) } async function confirmDelete() { - if (!pendingDelete || mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (!pendingDelete || mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return const target = pendingDelete mutationLock.current = true loadEpoch.current += 1 @@ -2248,7 +2253,7 @@ function FirewallPageInner() { }, [filteredRules]) async function reorderRules(activeId: string, overId: string) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return const source = isLive ? liveRules : mockRules const next = reorderWithinGroup(source, activeId, overId) if (!next) { @@ -2297,7 +2302,7 @@ function FirewallPageInner() { } function openAdd() { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return setFormError(null) if (!requireWriteServer()) return const table = tableOfGroup(chainGroup) @@ -2309,14 +2314,14 @@ function FirewallPageInner() { } function openEdit(r: FirewallRule) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return setFormError(null) setEditingRule(r) setRuleSheetOpen(true) } async function saveRule(payload: RuleWritePayload) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return const table = (editingRule?.table ?? tableOfGroup(chainGroup) ?? "filter") as FirewallTable const family = editingRule?.id ? ruleFamily(editingRule as FirewallRule) : ipFamily const serverId = editingRule?.serverId ?? requireWriteServer() @@ -2387,7 +2392,7 @@ function FirewallPageInner() { } function openAddAddr(list?: string) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return setFormError(null) if (!requireWriteServer()) return setEditingAddr(list ? { list } : null) @@ -2395,14 +2400,14 @@ function FirewallPageInner() { } function openEditAddr(e: AddressListEntry) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return setFormError(null) setEditingAddr(e) setAddrSheetOpen(true) } async function toggleAddr(e: AddressListEntry) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return if (isLive) { const key = addressKey(e) if (!key) return @@ -2439,7 +2444,7 @@ function FirewallPageInner() { timeout?: string disabled?: boolean }) { - if (mutationLock.current || busy || historyRestoring || firewallIncomplete) return + if (mutationLock.current || busy || historyRestoring || firewallWriteBlocked) return const family = editingAddr?.id ? addressFamily(editingAddr as AddressListEntry) : ipFamily const serverId = editingAddr?.serverId ?? requireWriteServer() if (!serverId) return @@ -2540,7 +2545,7 @@ function FirewallPageInner() { setHistoryOpen(true) void loadRevisions() }} - disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring || failures.some(f => f.serverId === historyServerId)} + disabled={!isLive || !historyServerId || dataLoading || busy || togglePending || deletePending || movePending || historyRestoring || !readTrusted || failures.some(f => f.serverId === historyServerId)} title={!historyServerId ? "Выберите сервер, чтобы смотреть историю" : "История версий и откат на CHR"} > @@ -2549,7 +2554,7 @@ function FirewallPageInner() { - @@ -2565,6 +2570,8 @@ function FirewallPageInner() { )} + {isLive && hasLoaded && !readTrusted &&

Изменения Firewall заблокированы до успешного чтения актуальных данных. Последний список сохранён.

} +
openAddAddr()} onAddToList={(list) => openAddAddr(list)} onToggle={(e) => { void toggleAddr(e) }} @@ -2708,11 +2715,11 @@ function FirewallPageInner() { { void toggleRule(id) }} onEdit={openEdit} onDelete={(item) => requestDelete({ kind: "rule", item })} - onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring && !firewallIncomplete ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined} + onReorder={canReorder && !togglePending && !deletePending && !movePending && !busy && !historyRestoring && !firewallWriteBlocked ? (activeId, overId) => { void reorderRules(activeId, overId) } : undefined} showServer={showServerCol} /> @@ -2807,7 +2814,7 @@ function FirewallPageInner() { Отмена - { e.preventDefault(); void confirmDelete() }}>Удалить + { e.preventDefault(); void confirmDelete() }}>Удалить diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index 71952b16..59db4782 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -84,3 +84,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.42: Filters1000/120894 JSON bytes →50 rows/2266 nodes/1880ms dev ready. Global order/граница страниц/полный PUT/403/поиск/сортировка/refresh/cache/clamp/320px/axe проверены;10+1 browser/type/lint/build прошли.12 manualPagination:true остаются. 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 остаются. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 8349e019..8baf8574 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 Firewall guard; затем сценарий/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: сценарий Firewall; затем 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 7ec7fa2d..2f2e8616 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.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 не изменены. + +29/29 main browser (data/partial/save/delete/toggle/move/history/large),2/2 новых stale cases и4/4 context прошли. Проверены503/некорректный DTO, неизменная строка, отключённые switch/menu/create/history, pending retry без writes, восстановление после успешного GET. TypeScript без incremental, ESLint нового теста и production build прошли. ESLint страницы имеет4 исходных set-state-in-effect errors (506/754/883/2081 в HEAD1e9889f) и2 unused warnings; сравнение ESLint API с HEAD подтвердило тот же набор в текущем файле (последняя строка2085). Это не полный lint pass; исправление прежних эффектов отдельно. + +Следующий блок12.3.45: большой редактор локального сценария Firewall — pagination/глобальный порядок/сохранение всего draft/симуляция/320px. Затем OSPF таблица+topology/optimizer.11 manualPagination:true остаются. Общая main/context после delta, lint cleanup Firewall и внешняя12.5 отдельно. Коммит локальный без push. + ## 12.3.43 — большая таблица Firewall и порядок (2026-10-04) ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain. DnD получает ID текущей страницы, кнопки44px up/down используют соседей полного отфильтрованного списка внутри server/family/table. Callback прежний: RouterOS destination рассчитывается по полному источнику, включая скрытые строки. DnD мышью/клавиатурой сохранён; action menu44px видим без hover. Переключателям добавлены доступные имена с rosId. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index c979e327..6d6095ac 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -542,3 +542,12 @@ ReUI pagination50/clamp/ScrollArea, закреплённые номер и chain Обнаружено прежнее ограничение: dataError после полного GET failure сохраняет кэш, но не блокирует действия, в отличие от partial failures. Следующий блок12.3.44: явная блокировка stale операций/повторного чтения/восстановления; затем большой редактор сценария и OSPF/optimizer.11 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 не изменены. + +29/29 main browser (data/partial/save/delete/toggle/move/history/large),2/2 новых stale cases и4/4 context прошли. Проверены503/некорректный DTO, неизменная строка, отключённые switch/menu/create/history, pending retry без writes, восстановление после успешного GET. TypeScript без incremental, ESLint нового теста и production build прошли. ESLint страницы имеет4 исходных set-state-in-effect errors (506/754/883/2081 в HEAD1e9889f) и2 unused warnings; сравнение ESLint API с HEAD подтвердило тот же набор в текущем файле (последняя строка2085). Это не полный lint pass; исправление прежних эффектов отдельно. + +Следующий блок12.3.45: большой редактор локального сценария Firewall — pagination/глобальный порядок/сохранение всего draft/симуляция/320px. Затем OSPF таблица+topology/optimizer.11 manualPagination:true остаются. Общая main/context после delta, lint cleanup Firewall и внешняя12.5 отдельно. Коммит локальный без push. + diff --git a/tests/e2e/firewall-stale.spec.ts b/tests/e2e/firewall-stale.spec.ts new file mode 100644 index 00000000..3f22abb8 --- /dev/null +++ b/tests/e2e/firewall-stale.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "@playwright/test" + +for (const failure of ["http", "invalid"] as const) { + test(`Firewall: ${failure} блокирует кэш до завершения успешного повтора`, 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 } + let failed = false, gated = false, writes = 0, started = false + let release: (() => void) | undefined + const gate = new Promise(resolve => { release = resolve }) + await page.route("**/health", r => r.fulfill({ json: { ok: true } })) + await page.route("**/api/**", async r => { + const p = new URL(r.request().url()).pathname + if (r.request().method() !== "GET") { writes++; return r.fulfill({ json: { ok: true } }) } + if (p === "/api/firewall/all") { + if (gated) { started = true; await gate } + if (failed) return r.fulfill(failure === "http" ? { status: 503, json: { error: "Чтение недоступно stale" } } : { json: { rules: "invalid", addressLists: [] } }) + return r.fulfill({ json: { rules: [rule], addressLists: [] } }) + } + return r.fulfill({ json: p === "/api/auth/config" ? { required: false } : 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") + const rows = page.locator("tr[data-row-id]") + const toggle = rows.getByRole("switch", { name: "Включено правило *1", exact: true }) + const history = page.getByRole("button", { name: "История", exact: true }) + await expect(toggle).toBeEnabled(); await expect(history).toBeEnabled() + failed = true + await page.getByRole("button", { name: "Обновить", exact: true }).click() + const status = page.getByRole("status").filter({ hasText: "Изменения Firewall заблокированы" }) + await expect(status).toBeVisible(); await expect(rows).toHaveCount(1) + await expect(toggle).toBeDisabled(); await expect(history).toBeDisabled() + await expect(page.getByRole("button", { name: "Новое правило", exact: true })).toBeDisabled() + await rows.getByRole("button", { name: "Действия: forward accept", exact: true }).click() + for (const name of ["Редактировать", "Отключить", "Удалить правило"]) await expect(page.getByRole("menuitem", { name, exact: true })).toBeDisabled() + await page.keyboard.press("Escape") + failed = false; gated = true + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect.poll(() => started).toBe(true) + await expect(status).toBeVisible(); await expect(toggle).toBeDisabled(); await expect(history).toBeDisabled() + expect(writes).toBe(0) + release!() + await expect(status).toHaveCount(0); await expect(toggle).toBeEnabled(); await expect(history).toBeEnabled() + await expect(rows).toHaveCount(1); expect(writes).toBe(0) + }) +}