fix(ui): учитывать права и состояние устройства при откате истории

This commit is contained in:
Denozordec committed 2026-10-13 19:45:39 +07:00
1 parent 5ce20e8892
commit c8f9c83f45
17 files changed
+388 -16

No files matched your search

+2
View File
@@ -1628,6 +1628,7 @@ function FiltersPage() {
}, [isLive, selectedServerId, apiFetch]) }, [isLive, selectedServerId, apiFetch])
const restoreRevision = useCallback(async (id: string) => { const restoreRevision = useCallback(async (id: string) => {
if (!can("mm:filters:write")) throw new Error("Нет прав на изменение конфигурации")
if (!isLive || !selectedServerId) throw new Error("Выберите устройство в режиме живых данных") if (!isLive || !selectedServerId) throw new Error("Выберите устройство в режиме живых данных")
if (applyLock.current || applyBusy || historyRestoring) throw new Error("Дождитесь завершения текущей операции") if (applyLock.current || applyBusy || historyRestoring) throw new Error("Дождитесь завершения текущей операции")
applyLock.current = true applyLock.current = true
@@ -2107,6 +2108,7 @@ function FiltersPage() {
revisions={revisions} revisions={revisions}
loading={historyLoading} loading={historyLoading}
restoring={historyRestoring} restoring={historyRestoring}
restoreDisabled={!can("mm:filters:write") || !isLive || !allServers.some(s => s.id === selectedServerId && s.enabled) || applyBusy || historyRestoring || movePending}
onRestore={restoreRevision} onRestore={restoreRevision}
/> />
</> </>
+4
View File
@@ -1,5 +1,7 @@
"use client" "use client"
import { can } from "@/lib/auth"
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
@@ -2040,6 +2042,7 @@ function FirewallPageInner() {
}, [isLive, historyServerId, apiFetch]) }, [isLive, historyServerId, apiFetch])
const restoreRevision = useCallback(async (id: string) => { const restoreRevision = useCallback(async (id: string) => {
if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации")
if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live") if (!isLive || !historyServerId) throw new Error("Выберите устройство в режиме Live")
if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall") if (mutationLock.current) throw new Error("Дождитесь завершения текущей операции Firewall")
if (!readTrusted) throw new Error("Обновите актуальные данные Firewall перед восстановлением") if (!readTrusted) throw new Error("Обновите актуальные данные Firewall перед восстановлением")
@@ -2837,6 +2840,7 @@ function FirewallPageInner() {
revisions={revisions} revisions={revisions}
loading={historyLoading} loading={historyLoading}
restoring={historyRestoring} restoring={historyRestoring}
restoreDisabled={!can("mm:network:write") || !isLive || !writeServer?.enabled || !readTrusted || failures.some(f => f.serverId === historyServerId) || dataLoading || busy || historyRestoring}
onRestore={restoreRevision} onRestore={restoreRevision}
/> />
</> </>
+4
View File
@@ -1,5 +1,7 @@
"use client" "use client"
import { can } from "@/lib/auth"
import { Suspense, useCallback, useEffect, useRef, useMemo, useState } from "react" import { Suspense, useCallback, useEffect, useRef, useMemo, useState } from "react"
import Link from "next/link" import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
@@ -294,6 +296,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
}, [isLive, historyServerId, apiFetch]) }, [isLive, historyServerId, apiFetch])
const restoreRevision = async (id: string) => { const restoreRevision = async (id: string) => {
if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации")
if (!historyServerId || !beginOperation(historyServerId)) throw new Error("Устройство не готово к восстановлению GRE") if (!historyServerId || !beginOperation(historyServerId)) throw new Error("Устройство не готово к восстановлению GRE")
setHistoryRestoring(true) setHistoryRestoring(true)
try { try {
@@ -934,6 +937,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
revisions={revisions} revisions={revisions}
loading={historyLoading} loading={historyLoading}
restoring={historyRestoring} restoring={historyRestoring}
restoreDisabled={!can("mm:network:write") || !historyServerId || !canManage(historyServerId)}
onRestore={restoreRevision} onRestore={restoreRevision}
/> />
+4
View File
@@ -1,5 +1,7 @@
"use client" "use client"
import { can } from "@/lib/auth"
import { z } from "zod" import { z } from "zod"
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
@@ -295,6 +297,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
}, [isLive, historyServerId, backendUrl]) }, [isLive, historyServerId, backendUrl])
const restoreRevision = async (id: string) => { const restoreRevision = async (id: string) => {
if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации")
if (!isLive || !historyServerId) return if (!isLive || !historyServerId) return
beginOperation(historyServerId) beginOperation(historyServerId)
setHistoryRestoring(true) setHistoryRestoring(true)
@@ -969,6 +972,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
revisions={revisions} revisions={revisions}
loading={historyLoading} loading={historyLoading}
restoring={historyRestoring} restoring={historyRestoring}
restoreDisabled={!can("mm:network:write") || !historyServerId || !canManage(historyServerId)}
onRestore={restoreRevision} onRestore={restoreRevision}
title="История конфигураций IPsec" title="История конфигураций IPsec"
itemLabel="конфигурация" itemLabel="конфигурация"
+4
View File
@@ -1,5 +1,7 @@
"use client" "use client"
import { can } from "@/lib/auth"
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"
import Link from "next/link" import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
@@ -308,6 +310,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
}, [isLive, historyServerId, backendUrl]) }, [isLive, historyServerId, backendUrl])
const restoreRevision = useCallback(async (id: string) => { const restoreRevision = useCallback(async (id: string) => {
if (!can("mm:network:write")) throw new Error("Нет прав на изменение конфигурации")
if (!isLive || !historyServerId || restoreLock.current) return if (!isLive || !historyServerId || restoreLock.current) return
restoreLock.current = true restoreLock.current = true
setHistoryRestoring(true) setHistoryRestoring(true)
@@ -1022,6 +1025,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
revisions={revisions} revisions={revisions}
loading={historyLoading} loading={historyLoading}
restoring={historyRestoring} restoring={historyRestoring}
restoreDisabled={!can("mm:network:write") || !isLive || !historyServerId || !displayServers.some(s => s.id === historyServerId && s.enabled) || loading || busy || deleteBusy || !!togglePending}
onRestore={restoreRevision} onRestore={restoreRevision}
/> />
</> </>
+4 -3
View File
@@ -69,7 +69,7 @@ export function ConfigHistorySheet({
onRetry, onRetry,
restoring, restoring,
onRestore, onRestore,
restoreDisabled = false, restoreDisabled = true,
}: { }: {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
@@ -80,7 +80,7 @@ export function ConfigHistorySheet({
loadError?: string | null loadError?: string | null
onRetry?: () => void onRetry?: () => void
restoring: boolean restoring: boolean
restoreDisabled?: boolean restoreDisabled: boolean
onRestore: (id: string) => Promise<void> | void onRestore: (id: string) => Promise<void> | void
}) { }) {
const [pending, setPending] = useState<ConfigRevisionDto | null>(null) const [pending, setPending] = useState<ConfigRevisionDto | null>(null)
@@ -123,7 +123,8 @@ export function ConfigHistorySheet({
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<div className="flex-1 min-h-0 overflow-y-auto px-6 py-5"> <div className="flex-1 min-h-0 overflow-y-auto px-6 py-5">
{loading ? ( {restoreDisabled && !busy && <p role="status" className="px-4 text-sm text-muted-foreground">Откат недоступен. Проверьте права на изменение и состояние выбранного устройства.</p>}
{loading ? (
<div role="status" className="flex items-center justify-center gap-2 py-16 text-sm text-muted-foreground"> <div role="status" className="flex items-center justify-center gap-2 py-16 text-sm text-muted-foreground">
<LoaderCircleIcon className="size-4 animate-spin" /> <LoaderCircleIcon className="size-4 animate-spin" />
Загрузка истории… Загрузка истории…
+12 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5 # 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). 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) ## 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). Подробный объём, протоколы, схема и ограничения: [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). Первоначальная мгновенная проверка 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 открыта. В 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 открыта.
+3 -3
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам # План реализации 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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-13 ## Актуальная точка продолжения — 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 и оставшиеся профильные сетевые ограничения открыты. Этапы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) ## 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). Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
+11 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации # 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). Общий план: [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 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. | | 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) ## 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. 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.
+14 -4
View File
@@ -1,10 +1,10 @@
# Оставшаяся внешняя приёмка — 2026-10-13 # Оставшаяся внешняя приёмка — 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 ## Следующая последовательность — 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 выполнена; не повторять её без изменений общей основы или нового основания. 2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций. 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). Первоначальная мгновенная проверка 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 открыта. В 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 открыта.
+11 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI # Этап 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. План: [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). Первоначальная мгновенная проверка 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 открыта. В 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 открыта.
+12 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10 # Тестовый сервер 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) ## 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). Подробный объём, протоколы, схема и ограничения: [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). Первоначальная мгновенная проверка 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 открыта. В 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 открыта.
@@ -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
}
@@ -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
}
@@ -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"
}
@@ -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"
]
}
+30
View File
@@ -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)
})}