From c8f9c83f45703c1aabe7c81ed83d4ebc461b8e6d Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 13 Oct 2026 19:45:39 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=83=D1=87=D0=B8=D1=82=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=D0=B5=20=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/filters/page.tsx | 2 + app/(main)/firewall/page.tsx | 4 + app/(main)/gre/page.tsx | 4 + app/(main)/ipsec/page.tsx | 4 + app/(main)/wireguard/page.tsx | 4 + components/config-history-sheet.tsx | 7 +- docs/ux-ui-external-acceptance.md | 14 +- docs/ux-ui-plan.md | 6 +- docs/ux-ui-progress.md | 12 +- docs/ux-ui-remaining-acceptance.md | 18 +- docs/ux-ui-stage-12.md | 12 +- docs/ux-ui-test-server.md | 14 +- .../history207-baseline-live-2026-10-13.json | 13 + .../history207-deployment-2026-10-13.json | 11 + .../acceptance/history207-e2e-2026-10-13.json | 14 ++ .../history207-live-2026-10-13.json | 235 ++++++++++++++++++ tests/e2e/history-restore-rights.spec.ts | 30 +++ 17 files changed, 388 insertions(+), 16 deletions(-) create mode 100644 docs/ux-ui/acceptance/history207-baseline-live-2026-10-13.json create mode 100644 docs/ux-ui/acceptance/history207-deployment-2026-10-13.json create mode 100644 docs/ux-ui/acceptance/history207-e2e-2026-10-13.json create mode 100644 docs/ux-ui/acceptance/history207-live-2026-10-13.json create mode 100644 tests/e2e/history-restore-rights.spec.ts diff --git a/app/(main)/filters/page.tsx b/app/(main)/filters/page.tsx index 41cbc9e4..65c8d3d9 100644 --- a/app/(main)/filters/page.tsx +++ b/app/(main)/filters/page.tsx @@ -1628,6 +1628,7 @@ function FiltersPage() { }, [isLive, selectedServerId, apiFetch]) const restoreRevision = useCallback(async (id: string) => { + if (!can("mm:filters:write")) throw new Error("Нет прав на изменение конфигурации") if (!isLive || !selectedServerId) throw new Error("Выберите устройство в режиме живых данных") if (applyLock.current || applyBusy || historyRestoring) throw new Error("Дождитесь завершения текущей операции") applyLock.current = true @@ -2107,6 +2108,7 @@ function FiltersPage() { revisions={revisions} loading={historyLoading} restoring={historyRestoring} + restoreDisabled={!can("mm:filters:write") || !isLive || !allServers.some(s => s.id === selectedServerId && s.enabled) || applyBusy || historyRestoring || movePending} onRestore={restoreRevision} /> diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index 1f744353..58022ef1 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -1,5 +1,7 @@ "use client" +import { can } from "@/lib/auth" + import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useSearchParams } from "next/navigation" import { PageHeader } from "@/components/page-header" @@ -2040,6 +2042,7 @@ function FirewallPageInner() { }, [isLive, historyServerId, apiFetch]) const restoreRevision = useCallback(async (id: string) => { + if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации") if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live") if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall") if (!readTrusted) throw new Error("Обновите актуальные данные Firewall перед восстановлением") @@ -2837,6 +2840,7 @@ function FirewallPageInner() { revisions={revisions} loading={historyLoading} restoring={historyRestoring} + restoreDisabled={!can("mm:network:write") || !isLive || !writeServer?.enabled || !readTrusted || failures.some(f => f.serverId === historyServerId) || dataLoading || busy || historyRestoring} onRestore={restoreRevision} /> diff --git a/app/(main)/gre/page.tsx b/app/(main)/gre/page.tsx index 18856992..5f71da9a 100644 --- a/app/(main)/gre/page.tsx +++ b/app/(main)/gre/page.tsx @@ -1,5 +1,7 @@ "use client" +import { can } from "@/lib/auth" + import { Suspense, useCallback, useEffect, useRef, useMemo, useState } from "react" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" @@ -294,6 +296,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on }, [isLive, historyServerId, apiFetch]) const restoreRevision = async (id: string) => { + if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации") if (!historyServerId || !beginOperation(historyServerId)) throw new Error("Устройство не готово к восстановлению GRE") setHistoryRestoring(true) try { @@ -934,6 +937,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on revisions={revisions} loading={historyLoading} restoring={historyRestoring} + restoreDisabled={!can("mm:network:write") || !historyServerId || !canManage(historyServerId)} onRestore={restoreRevision} /> diff --git a/app/(main)/ipsec/page.tsx b/app/(main)/ipsec/page.tsx index 1b39b2c7..91231853 100644 --- a/app/(main)/ipsec/page.tsx +++ b/app/(main)/ipsec/page.tsx @@ -1,5 +1,7 @@ "use client" +import { can } from "@/lib/auth" + import { z } from "zod" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useSearchParams } from "next/navigation" @@ -295,6 +297,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor }, [isLive, historyServerId, backendUrl]) const restoreRevision = async (id: string) => { + if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации") if (!isLive || !historyServerId) return beginOperation(historyServerId) setHistoryRestoring(true) @@ -969,6 +972,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor revisions={revisions} loading={historyLoading} restoring={historyRestoring} + restoreDisabled={!can("mm:network:write") || !historyServerId || !canManage(historyServerId)} onRestore={restoreRevision} title="История конфигураций IPsec" itemLabel="конфигурация" diff --git a/app/(main)/wireguard/page.tsx b/app/(main)/wireguard/page.tsx index 513a36dd..9cfff6cc 100644 --- a/app/(main)/wireguard/page.tsx +++ b/app/(main)/wireguard/page.tsx @@ -1,5 +1,7 @@ "use client" +import { can } from "@/lib/auth" + import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" @@ -308,6 +310,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf }, [isLive, historyServerId, backendUrl]) const restoreRevision = useCallback(async (id: string) => { + if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации") if (!isLive || !historyServerId || restoreLock.current) return restoreLock.current = true setHistoryRestoring(true) @@ -1022,6 +1025,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf revisions={revisions} loading={historyLoading} restoring={historyRestoring} + restoreDisabled={!can("mm:network:write") || !isLive || !historyServerId || !displayServers.some(s => s.id === historyServerId && s.enabled) || loading || busy || deleteBusy || !!togglePending} onRestore={restoreRevision} /> diff --git a/components/config-history-sheet.tsx b/components/config-history-sheet.tsx index abd3d2ef..88731469 100644 --- a/components/config-history-sheet.tsx +++ b/components/config-history-sheet.tsx @@ -69,7 +69,7 @@ export function ConfigHistorySheet({ onRetry, restoring, onRestore, - restoreDisabled = false, + restoreDisabled = true, }: { open: boolean onOpenChange: (open: boolean) => void @@ -80,7 +80,7 @@ export function ConfigHistorySheet({ loadError?: string | null onRetry?: () => void restoring: boolean - restoreDisabled?: boolean + restoreDisabled: boolean onRestore: (id: string) => Promise | void }) { const [pending, setPending] = useState(null) @@ -123,7 +123,8 @@ export function ConfigHistorySheet({
- {loading ? ( + {restoreDisabled && !busy &&

Откат недоступен. Проверьте права на изменение и состояние выбранного устройства.

} + {loading ? (
Загрузка истории… diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 464f944f..c76176f0 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md). @@ -696,7 +696,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -2500,3 +2500,13 @@ Actual Portal reader/admin JWT, deployed frontend ux-accessibility205/backend ux Первоначальная мгновенная проверка DOM containment после Tab иногда видела асинхронный focus transition Base UI; финальный тест ждёт стабильногоcontainment черезpoll. Product focus code не менялся. Это проверка listed states, не full keyboard/screen-reader certification всехформ. Source/runtime unchanged. Протокол: [actual keyboard24](ux-ui/acceptance/keyboard206-live-2026-10-13.json). В source review обнаружено: GRE caller не передаёт существующий restoreDisabled вобщий ConfigHistorySheet, defaultfalse. Backend write guard не отменяет необходимость корректного UI. Следующий207 — аудит всехcaller иread-only/disabled/device state запреты кнопки отката, профильныеfixtures иlive UI безapply.12.5 открыта. + +## 12.5.207 — права отката общей истории конфигураций (2026-10-13) + +Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката. + +Перваяширокая Filters readiness condition заблокировала прежнийдопустимыйretry послеapplied+failed read.3 regression failures обнаружены иисправлены: Filters используетpermission/enabled/operation guards, stale/failed read не добавляетновый запретrollback. Существующие правиласетевыхопераций сохранены.40 unique E2E(24 role/devicefixtures+16 existing restore/error/retry/lock),68987ms,0unexpected/skips. TypeScript/ESLint0errors;12 старыхunused import warnings Filters не менялись. Native apply толькоmocked tests, наCHR откатне выполнялся. + +Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись. + +Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index e4f3d717..15b495d0 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-13 -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. @@ -371,6 +371,6 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 1be8a601..09f28f45 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,16 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. | +## 12.5.207 — права отката общей истории конфигураций (2026-10-13) + +Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката. + +Перваяширокая Filters readiness condition заблокировала прежнийдопустимыйretry послеapplied+failed read.3 regression failures обнаружены иисправлены: Filters используетpermission/enabled/operation guards, stale/failed read не добавляетновый запретrollback. Существующие правиласетевыхопераций сохранены.40 unique E2E(24 role/devicefixtures+16 existing restore/error/retry/lock),68987ms,0unexpected/skips. TypeScript/ESLint0errors;12 старыхunused import warnings Filters не менялись. Native apply толькоmocked tests, наCHR откатне выполнялся. + +Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись. + +Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта. + ## 12.5.206 — живая клавиатура и возврат фокуса (2026-10-13) Actual Portal reader/admin JWT, deployed frontend ux-accessibility205/backend ux-lock200.6 сценариев ×1440/320×reader/admin =24: Tab skip-link→main-content; Enter/ArrowDown/Escape apps menu; Enter/Escape system monitor; keyboard server row menu иEscape возврат; reader Add disabled/admin server create Dialog; GRE history Sheet выбранного enabled CHR. Вdialog/sheet Tab проходит числоfocusable controls+3 иShift+Tab3, фокус остаётся внутри, Escape возвращает original trigger. Форму не заполняли/не применяли, config restore не нажимали, mutations0/page errors0/nooverflow; собственный Portal reader удалён204. diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index 475f28b2..f56adb1d 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,10 +1,10 @@ # Оставшаяся внешняя приёмка — 2026-10-13 -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. -Это актуальная матрица после 12.5.206. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.207. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-lock200`, frontend `ux-accessibility205`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-lock200`, frontend `ux-history207`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -26,7 +26,7 @@ ## Следующая последовательность — 2026-10-13 -1. **207 — history restore rights:** всеConfigHistorySheet caller, запретreader/disabled device, fixture+live UI безapply. +1. **208 — native read-only:** WireGuard private key flag GET permission иnetwork workspace write controls; SQL/native own fixtures, source guard/API/UI, безизменениячужихобъектов. 2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания. 3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций. @@ -1416,3 +1416,13 @@ Actual Portal reader/admin JWT, deployed frontend ux-accessibility205/backend ux Первоначальная мгновенная проверка DOM containment после Tab иногда видела асинхронный focus transition Base UI; финальный тест ждёт стабильногоcontainment черезpoll. Product focus code не менялся. Это проверка listed states, не full keyboard/screen-reader certification всехформ. Source/runtime unchanged. Протокол: [actual keyboard24](ux-ui/acceptance/keyboard206-live-2026-10-13.json). В source review обнаружено: GRE caller не передаёт существующий restoreDisabled вобщий ConfigHistorySheet, defaultfalse. Backend write guard не отменяет необходимость корректного UI. Следующий207 — аудит всехcaller иread-only/disabled/device state запреты кнопки отката, профильныеfixtures иlive UI безapply.12.5 открыта. + +## 12.5.207 — права отката общей истории конфигураций (2026-10-13) + +Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката. + +Перваяширокая Filters readiness condition заблокировала прежнийдопустимыйretry послеapplied+failed read.3 regression failures обнаружены иисправлены: Filters используетpermission/enabled/operation guards, stale/failed read не добавляетновый запретrollback. Существующие правиласетевыхопераций сохранены.40 unique E2E(24 role/devicefixtures+16 existing restore/error/retry/lock),68987ms,0unexpected/skips. TypeScript/ESLint0errors;12 старыхunused import warnings Filters не менялись. Native apply толькоmocked tests, наCHR откатне выполнялся. + +Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись. + +Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 761ad1a6..4464e173 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -3805,3 +3805,13 @@ Actual Portal reader/admin JWT, deployed frontend ux-accessibility205/backend ux Первоначальная мгновенная проверка DOM containment после Tab иногда видела асинхронный focus transition Base UI; финальный тест ждёт стабильногоcontainment черезpoll. Product focus code не менялся. Это проверка listed states, не full keyboard/screen-reader certification всехформ. Source/runtime unchanged. Протокол: [actual keyboard24](ux-ui/acceptance/keyboard206-live-2026-10-13.json). В source review обнаружено: GRE caller не передаёт существующий restoreDisabled вобщий ConfigHistorySheet, defaultfalse. Backend write guard не отменяет необходимость корректного UI. Следующий207 — аудит всехcaller иread-only/disabled/device state запреты кнопки отката, профильныеfixtures иlive UI безapply.12.5 открыта. + +## 12.5.207 — права отката общей истории конфигураций (2026-10-13) + +Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката. + +Перваяширокая Filters readiness condition заблокировала прежнийдопустимыйretry послеapplied+failed read.3 regression failures обнаружены иисправлены: Filters используетpermission/enabled/operation guards, stale/failed read не добавляетновый запретrollback. Существующие правиласетевыхопераций сохранены.40 unique E2E(24 role/devicefixtures+16 existing restore/error/retry/lock),68987ms,0unexpected/skips. TypeScript/ESLint0errors;12 старыхunused import warnings Filters не менялись. Native apply толькоmocked tests, наCHR откатне выполнялся. + +Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись. + +Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 0de93ca1..d13fbb91 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. | Параметр | Проверенное состояние | | --- | --- | @@ -708,7 +708,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-13:206 keyboard/focus live24 reader/admin1440/320 passed, Dialog/Sheet tab trap иEscape restoration, mutations0. Runtime frontend ux-accessibility205/backend ux-lock200. Следующий207 — права кнопки отката всех потребителей ConfigHistorySheet; GRE пока не передаёт restoreDisabled.12.5 открыта. +Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -2231,3 +2231,13 @@ Actual Portal reader/admin JWT, deployed frontend ux-accessibility205/backend ux Первоначальная мгновенная проверка DOM containment после Tab иногда видела асинхронный focus transition Base UI; финальный тест ждёт стабильногоcontainment черезpoll. Product focus code не менялся. Это проверка listed states, не full keyboard/screen-reader certification всехформ. Source/runtime unchanged. Протокол: [actual keyboard24](ux-ui/acceptance/keyboard206-live-2026-10-13.json). В source review обнаружено: GRE caller не передаёт существующий restoreDisabled вобщий ConfigHistorySheet, defaultfalse. Backend write guard не отменяет необходимость корректного UI. Следующий207 — аудит всехcaller иread-only/disabled/device state запреты кнопки отката, профильныеfixtures иlive UI безapply.12.5 открыта. + +## 12.5.207 — права отката общей истории конфигураций (2026-10-13) + +Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката. + +Перваяширокая Filters readiness condition заблокировала прежнийдопустимыйretry послеapplied+failed read.3 regression failures обнаружены иисправлены: Filters используетpermission/enabled/operation guards, stale/failed read не добавляетновый запретrollback. Существующие правиласетевыхопераций сохранены.40 unique E2E(24 role/devicefixtures+16 existing restore/error/retry/lock),68987ms,0unexpected/skips. TypeScript/ESLint0errors;12 старыхunused import warnings Filters не менялись. Native apply толькоmocked tests, наCHR откатне выполнялся. + +Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись. + +Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта. diff --git a/docs/ux-ui/acceptance/history207-baseline-live-2026-10-13.json b/docs/ux-ui/acceptance/history207-baseline-live-2026-10-13.json new file mode 100644 index 00000000..513bd231 --- /dev/null +++ b/docs/ux-ui/acceptance/history207-baseline-live-2026-10-13.json @@ -0,0 +1,13 @@ +{ + "date": "2026-10-13", + "block": "12.5.207", + "frontend": "ux-accessibility205", + "role": "reader", + "section": "gre", + "buttons": 50, + "enabledRestoreButtons": 50, + "realPortalJWT": true, + "restoreClicked": false, + "ownReaderRemoved": true, + "issueConfirmed": true +} diff --git a/docs/ux-ui/acceptance/history207-deployment-2026-10-13.json b/docs/ux-ui/acceptance/history207-deployment-2026-10-13.json new file mode 100644 index 00000000..6e60c315 --- /dev/null +++ b/docs/ux-ui/acceptance/history207-deployment-2026-10-13.json @@ -0,0 +1,11 @@ +{ + "date": "2026-10-13", + "block": "12.5.207", + "sourceFilesMatched": 527, + "productionBuildPassed": true, + "frontendImage": "mmapp-test/frontend:ux-history207", + "imageId": "sha256:b012f5b195bbbf4d600f7eed0fab4b15fe494a50e5a8baef03e08e5bc9c042b7e", + "backendImage": "mmapp-test/backend:ux-lock200", + "rollbackFrontendTag": "ux-accessibility205", + "nativeRestoresApplied": 0 +} diff --git a/docs/ux-ui/acceptance/history207-e2e-2026-10-13.json b/docs/ux-ui/acceptance/history207-e2e-2026-10-13.json new file mode 100644 index 00000000..deb2f803 --- /dev/null +++ b/docs/ux-ui/acceptance/history207-e2e-2026-10-13.json @@ -0,0 +1,14 @@ +{ + "date": "2026-10-13", + "block": "12.5.207", + "stats": { + "startTime": "2026-10-13T12:30:12.552Z", + "duration": 68986.603, + "expected": 40, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "errors": [], + "scope": "24 reader/writer/disabled history fixtures +16 existing restore/lock/error/retry; no native apply" +} diff --git a/docs/ux-ui/acceptance/history207-live-2026-10-13.json b/docs/ux-ui/acceptance/history207-live-2026-10-13.json new file mode 100644 index 00000000..764130ad --- /dev/null +++ b/docs/ux-ui/acceptance/history207-live-2026-10-13.json @@ -0,0 +1,235 @@ +{ + "date": "2026-10-13", + "block": "12.5.207", + "runtime": { + "frontend": "ux-history207", + "backend": "ux-lock200", + "portal": "1.3.1" + }, + "cases": [ + { + "section": "wireguard", + "role": "reader", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "ipsec", + "role": "reader", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "gre", + "role": "reader", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "firewall", + "role": "reader", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "filters", + "role": "reader", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "recursive-routes", + "role": "reader", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "wireguard", + "role": "admin", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "ipsec", + "role": "admin", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "gre", + "role": "admin", + "width": 1440, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "firewall", + "role": "admin", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "filters", + "role": "admin", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "recursive-routes", + "role": "admin", + "width": 1440, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "wireguard", + "role": "reader", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "ipsec", + "role": "reader", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "gre", + "role": "reader", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "firewall", + "role": "reader", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "filters", + "role": "reader", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "recursive-routes", + "role": "reader", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 0, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "wireguard", + "role": "admin", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "ipsec", + "role": "admin", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "gre", + "role": "admin", + "width": 320, + "buttons": 50, + "enabledRestoreButtons": 50, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "firewall", + "role": "admin", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "filters", + "role": "admin", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + }, + { + "section": "recursive-routes", + "role": "admin", + "width": 320, + "buttons": 1, + "enabledRestoreButtons": 1, + "expectedPermissionState": true, + "applied": false + } + ], + "apiMocks": false, + "realPortalJWT": true, + "mutationRequests": 0, + "ownReaderRemoved": true, + "passed": true, + "bounds": [ + "No restore applied; populated historical metadata, reader/admin action state and focus only" + ] +} diff --git a/tests/e2e/history-restore-rights.spec.ts b/tests/e2e/history-restore-rights.spec.ts new file mode 100644 index 00000000..d04e3913 --- /dev/null +++ b/tests/e2e/history-restore-rights.spec.ts @@ -0,0 +1,30 @@ +import {test,expect,type Page} from '@playwright/test' +const sections=['wireguard','ipsec','gre','firewall','filters','recursive-routes'] as const +function token(writer:boolean){return [Buffer.from('{}').toString('base64url'),Buffer.from(JSON.stringify({sub:'own207',apps:['mm'],permissions:['mm:dashboard:read','mm:servers:read','mm:network:read','mm:filters:read',...(writer?['mm:network:write','mm:filters:write']:[])],exp:Math.floor(Date.now()/1000)+600})).toString('base64url'),'fixture'].join('.')} +async function setup(page:Page,section:string,writer:boolean,enabled=true){let writes=0 + await page.addInitScript(t=>localStorage.setItem('mmapp_token',t),token(writer)) + await page.route('**/health',r=>r.fulfill({json:{ok:true}})) + await page.route('**/api/**',r=>{const path=new URL(r.request().url()).pathname;if(r.request().method()!=='GET'){writes++;return r.fulfill({status:403,json:{error:'Unexpected mutation in read-only history fixture'}})} + const json=path.endsWith('/revisions')?{revisions:[{id:'own207-revision',serverId:'1',section,source:'apply',fingerprint:'fixture',createdAt:'2026-10-13T00:00:00Z',note:null,itemCount:1}]}: + path==='/api/auth/config'?{required:true,portal_url:'http://portal.own.invalid'}: + path==='/api/servers'?[{id:1,name:'Own207 router',host:'192.0.2.207',type:'exit-node',enabled,country:'RU',site:'OWN',asn:'',status:'online',latency:1}]: + path==='/api/wireguard'?{interfaces:[]}: + path==='/api/ipsec'?{servers:[],clients:[]}: + path==='/api/gre/tunnels'||path==='/api/filters/gre-tunnels'?{tunnels:[]}: + path==='/api/firewall/all'?{rules:[],addressLists:[]}: + path==='/api/filters/rules'?{rulesets:[{serverId:'1',rules:[]}],greTunnels:[],live:true}: + path==='/api/recursive-routes/catalog'?{serverId:1,routes:[],live:true,stale:false}: + path==='/api/recursive-routes/gateways/catalog'?{serverId:1,gateways:[]}: + path==='/api/evobgp/settings'?{enabled:false,baseUrl:'',secretConfigured:false}:{} + return r.fulfill({json})}) + await page.goto('/'+section+'?server=1');return ()=>writes +} +for(const width of [1440,320])for(const section of sections)test(`История207 reader не может откатить ${section} ${width}`,async({page})=>{ + await page.setViewportSize({width,height:1000});const writes=await setup(page,section,false) + const history=page.getByRole('button',{name:'История',exact:true});await expect(history).toBeEnabled();await history.click();const sheet=page.getByRole('dialog');const restore=sheet.getByRole('button',{name:'Откатить',exact:true});await expect(restore).toBeDisabled();await expect(sheet.getByRole('status')).toContainText('Откат недоступен');await restore.evaluate(b=>(b as HTMLButtonElement).click());expect(writes()).toBe(0);expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true) +}) +for(const section of sections){test(`История207 writer сохраняет подтверждение ${section}`,async({page})=>{ + const writes=await setup(page,section,true);const history=page.getByRole('button',{name:'История',exact:true});await expect(history).toBeEnabled();await history.click();const restore=page.getByRole('dialog').getByRole('button',{name:'Откатить',exact:true});await expect(restore).toBeEnabled();await restore.click();await expect(page.getByRole('alertdialog')).toBeVisible();await page.getByRole('alertdialog').getByRole('button',{name:'Отмена',exact:true}).click();expect(writes()).toBe(0) + });test(`История207 выключенный сервер блокирует откат ${section}`,async({page})=>{ + const writes=await setup(page,section,true,false);const history=page.getByRole('button',{name:'История',exact:true});await expect(history).toBeVisible();if(await history.isEnabled()){await history.click();await expect(page.getByRole('dialog').getByRole('button',{name:'Откатить',exact:true})).toBeDisabled()}expect(writes()).toBe(0) + })}