fix(wireguard): сохранять меню действий при перерисовке

This commit is contained in:
Denozordec committed 2026-10-08 22:32:57 +07:00
1 parent 4c4ba02b44
commit 6a46fb1e40
13 files changed
+466 -85

No files matched your search

+90 -79
View File
@@ -2,7 +2,7 @@
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo, type ReactNode } from "react"
import { createContext, useContext, useEffect, useMemo, type ReactNode } from "react"
import {
type ColumnDef,
useTable,
@@ -58,6 +58,76 @@ interface WireguardDataGridProps {
onExportPeer?: (iface: WgIfaceWithServer, peerId: string) => void
}
type WireguardActions = Pick<WireguardDataGridProps, "onExport" | "onAddPeer" | "onToggle" | "toggleBusy" | "onDelete">
const WireguardActionsContext = createContext<WireguardActions | null>(null)
// Preserve menu state while callbacks and the current row data update.
function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } }) {
const actions = useContext(WireguardActionsContext)
if (!actions) throw new Error("WireGuard actions context is missing")
const { onExport, onAddPeer, onToggle, toggleBusy, onDelete } = actions
const iface = row.original
return (
<div
className="flex justify-end"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"transition-[background-color,color,border-color]",
"group-hover/row:opacity-100 focus-visible:opacity-100",
"data-popup-open:opacity-100 data-popup-open:bg-muted",
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Действия: ${iface.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(iface)}>
<CodeXmlIcon className="size-4" />
Экспорт
</DropdownMenuItem>
{onAddPeer && (
<DropdownMenuItem onClick={() => onAddPeer(iface)}>
<PlusIcon className="size-4" />
Добавить пира
</DropdownMenuItem>
)}
{onToggle && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={toggleBusy} onClick={() => onToggle(iface)}>
<PowerIcon className="size-4" />
{iface.enabled ? "Отключить" : "Включить"}
</DropdownMenuItem>
</>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => onDelete(iface)}>
<Trash2Icon className="size-4" />
Удалить
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
function WireguardDataGrid({
interfaces,
compactServer = false,
@@ -198,68 +268,7 @@ function WireguardDataGrid({
{
id: "actions",
header: () => <span className="sr-only">Действия</span>,
cell: ({ row }) => {
const iface = row.original
return (
<div
className="flex justify-end"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"transition-[background-color,color,border-color]",
"group-hover/row:opacity-100 focus-visible:opacity-100",
"data-popup-open:opacity-100 data-popup-open:bg-muted",
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Действия: ${iface.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(iface)}>
<CodeXmlIcon className="size-4" />
Экспорт
</DropdownMenuItem>
{onAddPeer && (
<DropdownMenuItem onClick={() => onAddPeer(iface)}>
<PlusIcon className="size-4" />
Добавить пира
</DropdownMenuItem>
)}
{onToggle && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={toggleBusy} onClick={() => onToggle(iface)}>
<PowerIcon className="size-4" />
{iface.enabled ? "Отключить" : "Включить"}
</DropdownMenuItem>
</>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => onDelete(iface)}>
<Trash2Icon className="size-4" />
Удалить
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
cell: WireguardActionsCell,
enableSorting: false,
size: 72,
meta: {
@@ -268,7 +277,7 @@ function WireguardDataGrid({
},
},
],
[compactServer, onExport, onAddPeer, onToggle, toggleBusy, onDelete, onDeletePeer, onExportPeer],
[compactServer, onDeletePeer, onExportPeer],
)
const table = useTable({
@@ -302,21 +311,23 @@ function WireguardDataGrid({
}
return (
<DataGridShell
table={table}
recordCount={interfaces.length}
pagination={interfaces.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
tableClassNames={{
headerRow: "border-b border-border",
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
}}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
<WireguardActionsContext.Provider value={{ onExport, onAddPeer, onToggle, toggleBusy, onDelete }}>
<DataGridShell
table={table}
recordCount={interfaces.length}
pagination={interfaces.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
tableClassNames={{
headerRow: "border-b border-border",
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
}}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</WireguardActionsContext.Provider>
)
}
+13 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Статус на2026-10-08: backend/frontend3722fad, backend healthy. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md).
Статус на2026-10-08: backend/frontend3722fad, backend healthy. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. UI создание WG принято в границах12.5.61, WireGuard toggle не завершён; локальная устойчивость его меню ещё не развёрнута. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md).
## Паспорт стенда
@@ -453,3 +453,15 @@ DELETE200/200, собственные GRE/IP/connected routes удалены; в
Итоговый harness exit0: afterBrowserDelete пуст на обеих сторонах, выбранные WG/peers/IP/routes/IPv4 Firewall поля совпали с baseline. Fallback не понадобился для peers/IP/interfaces; ephemeral контейнер удалён, остались три основные Compose-службы. WireGuard revisions намеренно сохранены как история операций. [Smoke](ux-ui/acceptance/deploy-browser-3722fad-2026-10-08.json): dashboard/servers/uptime/settings/filters/alerts200,320px fit, JS/API errors0.
Приняты успешное обычное удаление own интерфейса с IPv4 адресом и пиром и соответствующий UI результат. Partial failures/read errors/сохранение чужих WG объектов проверены локальными тестами59; на этом стенде такие отказы не инжектировались. IPv6 lifecycle, concurrency/multi-instance, throughput, Telegram/Portal и остальные внешние ограничения остаются открытыми. Следующий12.5.61 — реальные WireGuard действия через UI: создание интерфейса и пира, переключение и очистка собственной пары; ключи не публиковать. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
## 12.5.61 — UI создание WireGuard и локальное исправление нестабильного меню (2026-10-08)
[Первый native observer](ux-ui/acceptance/wg-ui-first-attempt-native-2026-10-08.json): собственная пара mm-acceptance-wg-ui-1791472667527, свободные UDP51879/198.18.255.240/30, интерфейсы и пиры созданы только через production формы приложения. Observer не создавал конфигурацию. Оба интерфейса/IP/native key generation и поля peers/endpoint/allowed/keepalive/comment подтвердились, native peer key association и первый двусторонний Ping3/3 прошли. Публичные ключи переданы в форму в памяти, ключи не включены в отчёты. Создание интерфейса наCHR1 выполнено наПК, наCHR2 при320px. [Мобильная форма](ux-ui/screens/wg-ui-create-server2-320-2026-10-08.png) сохранена без ключей и визуально просмотрена; этот снимок второй попытки, не доказательство переключения.
Полный сценарий НЕ завершён: первая попытка не получила PATCH переключения; вторая, mm-acceptance-wg-ui-1791472879650, после создания интерфейсов остановилась на Добавить пира. Playwright зафиксировал unstable→detached DOM menuitem при перерисовке. Callback props пересоздавали columns/inline actions cell, React сбрасывал Base UI menu. Временные browser harness имеют эти ограничения; успешный финальный browser report не создавался и не заявляется.
Локально components/data-grids/wireguard-data-grid.tsx адаптирован по уже принятому GRE образцу: стабильный WireguardActionsCell и типизированный context передают свежие onExport/onAddPeer/onToggle/onDelete/toggleBusy. Данные строки остаются текущими; composition, styles, row key и installed ReUI APIs сохранены. Tests/e2e/wireguard-menu-refresh.spec.ts воспроизвёл исчезнувшее меню на прежнем коде; после исправления меню переживает pending refresh, показывает новое enabled state и вызывает PATCH с актуальным disabled=false. Вместе с wireguard-tables.spec.ts —6/6 passed. ESLint и scoped TypeScript passed. Первый TypeScript запуск совпал с генерацией .next/dev/types и получил временные syntax errors; повтор после остановки Next прошёл. ReUI checklist проверен, общие primitives не менялись.
Первая пара очищена observer, выбранный baseline полностью совпал. Во второй попытке [observer получил401](ux-ui/acceptance/wg-ui-second-observer-error-2026-10-08.json) на завершающем native чтении до cleanup; причина не установлена, пароли не менялись. Повторное чтение через приложение успешно. [Штатный API cleanup](ux-ui/acceptance/wg-ui-second-api-cleanup-2026-10-08.json) проверил точные name/ownership comment и удалил оба own интерфейса DELETE200/200. [Независимая native проверка](ux-ui/acceptance/wg-ui-final-native-cleanup-2026-10-08.json) подтвердила отсутствие own WG/peers/IP/routes на обоих CHR, total IPv4 addresses1/2 и routes4/5; WG interfaces/peers0. Второй baseline был только в памяти и потерян при выходе observer: точное равенство его baseline не заявляется. [DB controls](ux-ui/acceptance/wg-ui-db-controls-2026-10-08.json) servers2/speedHistory9/retention14/migrations9 совпали. Эфемерные контейнеры завершены, остались три основные службы, backend healthy. Audit revisions намеренно сохранены.
Runtime backend/frontend3722fad, локальное меню ещё не развёрнуто. Следующий12.5.62 — frontend Docker deploy и полный UI create interface/peer→disable/enable→повторный handshake/Ping→delete на новой реальной паре, без повторных открытий меню. Перед повтором observer должен сохранить приватный baseline на сервере и продолжать независимую cleanup каждого устройства при ошибке финального state read; запуск browser только после подтверждения готовности нового observer. Переключение, экспорт/import, throughput/RBAC и прочие внешние ограничения остаются открытыми. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
+2 -2
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-08: 12.5.60 развернул backend/frontend3722fad и подтвердил исправление удаления WG на новой реальной паре CHR: обычный UI DELETE200/200 удалил интерфейсы, peers и IPv4 addresses без предварительной очистки. Native состояние проверено до fallback, выбранный baseline и DB controls совпали, backend healthy; production builds/TypeScript и smoke6 passed. Следующий12.5.61 — WireGuard через UI: создание интерфейса/пира, переключение и очистка собственной реальной пары; секреты не публиковать. Этап12.5 открыт.
Актуальное продолжение 2026-10-08: 12.5.61 подтвердил UI создание WireGuard interfaces/peers, native поля, handshake и первый Ping; переключение не завершено из-за remount меню (DOM detached). Локально WireGuard actions вынесен в стабильный renderer/context, red→green regression и profile6/6/TypeScript/ESLint passed. Обе попытки own объектов очищены, native отсутствие остатков и DB controls подтверждены; transient observer401/recovery отражены отдельно. Runtime backend/frontend3722fad остаётся прежним. Следующий12.5.62 — frontend deploy и полный UI create/peer/toggle/Ping/delete без повторного открытия меню; observer cleanup защитить от ошибок завершающего чтения. Этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-08
Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend/frontend3722fad и два CHR. Внешние проверки подтвердили PG backup/restore/rollback, остановку фоновой работы для одного экземпляра, API JWT/RBAC и браузерные состояния восстановления. Native TCP/UDP speed, история и UI проверены в границах журнала; доступ ко второму CHR восстановлен пользователем. Очередь Telegram проверена на реальном PG: dedupe, конкурентный claim, retry/version, ошибки сохранения и неопределённый исход; ответы отправителя подменялись, реальной доставки это не доказывает. Блоки12.5.43–45 подтвердили browser retry/checkpoint и развернули понятное пояснение HTTP500. 12.5.46 подтвердил browser JWT/RBAC очереди на реальном HTTP/PG; 12.5.47 подтвердил изоляцию checkpoint при смене двух реальных HTTP/PG источников в конфигурации local; 12.5.48 подтвердил scoped axe/клавиатуру в обеих темах и320px; 12.5.49 собрал Q-матрицу и подтвердил отсутствие настройки Telegram; 12.5.50 подтвердил GET-only инвентаризацию: VPN/BGP/OSPF пусты, роли home-router/home-router; По уточнению пользователя12.5.51 создал реальные GRE туннели и подтвердил lifecycle/связность; обнаружена потеря peer IP, 12.5.52 локально исправил сохранение peer metadata, профильные проверки passed; 12.5.53 развернул исправление и подтвердил его на реальной GRE паре/API/UI; 12.5.54 локально исправил устойчивость меню GRE, профильные проверки passed; 12.5.55 развернул frontend731803b и подтвердил устойчивость меню на реальной GRE паре; 12.5.56 локально адаптировал внутренние IP поля GRE для320px, обе темы и профильные проверки passed; 12.5.57 развернул frontendfd521f6 и подтвердил мобильную GRE форму на реальной паре CHR; 12.5.58 подтвердил настоящую WireGuard пару, handshake/Ping/native/UI и финальную очистку; 12.5.59 подтвердил оставшиеся peers/IP после удаления WG и локально исправил backend; 12.5.60 развернул backend/frontend3722fad и подтвердил полное обычное удаление WG; следующий12.5.61 — WireGuard UI создание/переключение собственной реальной пары. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения.
Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend/frontend3722fad и два CHR. Внешние проверки подтвердили PG backup/restore/rollback, остановку фоновой работы для одного экземпляра, API JWT/RBAC и браузерные состояния восстановления. Native TCP/UDP speed, история и UI проверены в границах журнала; доступ ко второму CHR восстановлен пользователем. Очередь Telegram проверена на реальном PG: dedupe, конкурентный claim, retry/version, ошибки сохранения и неопределённый исход; ответы отправителя подменялись, реальной доставки это не доказывает. Блоки12.5.43–45 подтвердили browser retry/checkpoint и развернули понятное пояснение HTTP500. 12.5.46 подтвердил browser JWT/RBAC очереди на реальном HTTP/PG; 12.5.47 подтвердил изоляцию checkpoint при смене двух реальных HTTP/PG источников в конфигурации local; 12.5.48 подтвердил scoped axe/клавиатуру в обеих темах и320px; 12.5.49 собрал Q-матрицу и подтвердил отсутствие настройки Telegram; 12.5.50 подтвердил GET-only инвентаризацию: VPN/BGP/OSPF пусты, роли home-router/home-router; По уточнению пользователя12.5.51 создал реальные GRE туннели и подтвердил lifecycle/связность; обнаружена потеря peer IP, 12.5.52 локально исправил сохранение peer metadata, профильные проверки passed; 12.5.53 развернул исправление и подтвердил его на реальной GRE паре/API/UI; 12.5.54 локально исправил устойчивость меню GRE, профильные проверки passed; 12.5.55 развернул frontend731803b и подтвердил устойчивость меню на реальной GRE паре; 12.5.56 локально адаптировал внутренние IP поля GRE для320px, обе темы и профильные проверки passed; 12.5.57 развернул frontendfd521f6 и подтвердил мобильную GRE форму на реальной паре CHR; 12.5.58 подтвердил настоящую WireGuard пару, handshake/Ping/native/UI и финальную очистку; 12.5.59 подтвердил оставшиеся peers/IP после удаления WG и локально исправил backend; 12.5.60 развернул backend/frontend3722fad и подтвердил полное обычное удаление WG; 12.5.61 подтвердил UI создание WG и локально исправил remount меню; переключение осталось открытым. Следующий12.5.62 — frontend deploy и полный UI lifecycle с независимой защищённой очисткой. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения.
Выполненные внешние сценарии не заменяют приёмку оставшихся интеграций и условий эксплуатации. Датированные продолжения ниже — история; точный следующий шаг указан в начале [журнала](ux-ui-progress.md).
+14 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-08: 12.5.60 развернул backend/frontend3722fad и подтвердил исправление удаления WG на новой реальной паре CHR: обычный UI DELETE200/200 удалил интерфейсы, peers и IPv4 addresses без предварительной очистки. Native состояние проверено до fallback, выбранный baseline и DB controls совпали, backend healthy; production builds/TypeScript и smoke6 passed. Следующий12.5.61 — WireGuard через UI: создание интерфейса/пира, переключение и очистка собственной реальной пары; секреты не публиковать. Этап12.5 открыт.
Актуальное продолжение 2026-10-08: 12.5.61 подтвердил UI создание WireGuard interfaces/peers, native поля, handshake и первый Ping; переключение не завершено из-за remount меню (DOM detached). Локально WireGuard actions вынесен в стабильный renderer/context, red→green regression и profile6/6/TypeScript/ESLint passed. Обе попытки own объектов очищены, native отсутствие остатков и DB controls подтверждены; transient observer401/recovery отражены отдельно. Runtime backend/frontend3722fad остаётся прежним. Следующий12.5.62 — frontend deploy и полный UI create/peer/toggle/Ping/delete без повторного открытия меню; observer cleanup защитить от ошибок завершающего чтения. Этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -34,7 +34,19 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–60 и ограничения ниже. Runtime backend/frontend3722fad. |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–61 и ограничения ниже. Runtime backend/frontend3722fad. |
## 12.5.61 — UI создание WireGuard и локальное исправление нестабильного меню (2026-10-08)
[Первый native observer](ux-ui/acceptance/wg-ui-first-attempt-native-2026-10-08.json): собственная пара mm-acceptance-wg-ui-1791472667527, свободные UDP51879/198.18.255.240/30, интерфейсы и пиры созданы только через production формы приложения. Observer не создавал конфигурацию. Оба интерфейса/IP/native key generation и поля peers/endpoint/allowed/keepalive/comment подтвердились, native peer key association и первый двусторонний Ping3/3 прошли. Публичные ключи переданы в форму в памяти, ключи не включены в отчёты. Создание интерфейса наCHR1 выполнено наПК, наCHR2 при320px. [Мобильная форма](ux-ui/screens/wg-ui-create-server2-320-2026-10-08.png) сохранена без ключей и визуально просмотрена; этот снимок второй попытки, не доказательство переключения.
Полный сценарий НЕ завершён: первая попытка не получила PATCH переключения; вторая, mm-acceptance-wg-ui-1791472879650, после создания интерфейсов остановилась на Добавить пира. Playwright зафиксировал unstable→detached DOM menuitem при перерисовке. Callback props пересоздавали columns/inline actions cell, React сбрасывал Base UI menu. Временные browser harness имеют эти ограничения; успешный финальный browser report не создавался и не заявляется.
Локально components/data-grids/wireguard-data-grid.tsx адаптирован по уже принятому GRE образцу: стабильный WireguardActionsCell и типизированный context передают свежие onExport/onAddPeer/onToggle/onDelete/toggleBusy. Данные строки остаются текущими; composition, styles, row key и installed ReUI APIs сохранены. Tests/e2e/wireguard-menu-refresh.spec.ts воспроизвёл исчезнувшее меню на прежнем коде; после исправления меню переживает pending refresh, показывает новое enabled state и вызывает PATCH с актуальным disabled=false. Вместе с wireguard-tables.spec.ts —6/6 passed. ESLint и scoped TypeScript passed. Первый TypeScript запуск совпал с генерацией .next/dev/types и получил временные syntax errors; повтор после остановки Next прошёл. ReUI checklist проверен, общие primitives не менялись.
Первая пара очищена observer, выбранный baseline полностью совпал. Во второй попытке [observer получил401](ux-ui/acceptance/wg-ui-second-observer-error-2026-10-08.json) на завершающем native чтении до cleanup; причина не установлена, пароли не менялись. Повторное чтение через приложение успешно. [Штатный API cleanup](ux-ui/acceptance/wg-ui-second-api-cleanup-2026-10-08.json) проверил точные name/ownership comment и удалил оба own интерфейса DELETE200/200. [Независимая native проверка](ux-ui/acceptance/wg-ui-final-native-cleanup-2026-10-08.json) подтвердила отсутствие own WG/peers/IP/routes на обоих CHR, total IPv4 addresses1/2 и routes4/5; WG interfaces/peers0. Второй baseline был только в памяти и потерян при выходе observer: точное равенство его baseline не заявляется. [DB controls](ux-ui/acceptance/wg-ui-db-controls-2026-10-08.json) servers2/speedHistory9/retention14/migrations9 совпали. Эфемерные контейнеры завершены, остались три основные службы, backend healthy. Audit revisions намеренно сохранены.
Runtime backend/frontend3722fad, локальное меню ещё не развёрнуто. Следующий12.5.62 — frontend Docker deploy и полный UI create interface/peer→disable/enable→повторный handshake/Ping→delete на новой реальной паре, без повторных открытий меню. Перед повтором observer должен сохранить приватный baseline на сервере и продолжать независимую cleanup каждого устройства при ошибке финального state read; запуск browser только после подтверждения готовности нового observer. Переключение, экспорт/import, throughput/RBAC и прочие внешние ограничения остаются открытыми. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
## 12.5.60 — развёртывание и реальное полное удаление WireGuard (2026-10-08)
+13 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-08: 12.5.60 развернул backend/frontend3722fad и подтвердил исправление удаления WG на новой реальной паре CHR: обычный UI DELETE200/200 удалил интерфейсы, peers и IPv4 addresses без предварительной очистки. Native состояние проверено до fallback, выбранный baseline и DB controls совпали, backend healthy; production builds/TypeScript и smoke6 passed. Следующий12.5.61 — WireGuard через UI: создание интерфейса/пира, переключение и очистка собственной реальной пары; секреты не публиковать. Этап12.5 открыт.
Актуальное продолжение 2026-10-08: 12.5.61 подтвердил UI создание WireGuard interfaces/peers, native поля, handshake и первый Ping; переключение не завершено из-за remount меню (DOM detached). Локально WireGuard actions вынесен в стабильный renderer/context, red→green regression и profile6/6/TypeScript/ESLint passed. Обе попытки own объектов очищены, native отсутствие остатков и DB controls подтверждены; transient observer401/recovery отражены отдельно. Runtime backend/frontend3722fad остаётся прежним. Следующий12.5.62 — frontend deploy и полный UI create/peer/toggle/Ping/delete без повторного открытия меню; observer cleanup защитить от ошибок завершающего чтения. Этап12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -1618,3 +1618,15 @@ DELETE200/200, собственные GRE/IP/connected routes удалены; в
Итоговый harness exit0: afterBrowserDelete пуст на обеих сторонах, выбранные WG/peers/IP/routes/IPv4 Firewall поля совпали с baseline. Fallback не понадобился для peers/IP/interfaces; ephemeral контейнер удалён, остались три основные Compose-службы. WireGuard revisions намеренно сохранены как история операций. [Smoke](ux-ui/acceptance/deploy-browser-3722fad-2026-10-08.json): dashboard/servers/uptime/settings/filters/alerts200,320px fit, JS/API errors0.
Приняты успешное обычное удаление own интерфейса с IPv4 адресом и пиром и соответствующий UI результат. Partial failures/read errors/сохранение чужих WG объектов проверены локальными тестами59; на этом стенде такие отказы не инжектировались. IPv6 lifecycle, concurrency/multi-instance, throughput, Telegram/Portal и остальные внешние ограничения остаются открытыми. Следующий12.5.61 — реальные WireGuard действия через UI: создание интерфейса и пира, переключение и очистка собственной пары; ключи не публиковать. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
## 12.5.61 — UI создание WireGuard и локальное исправление нестабильного меню (2026-10-08)
[Первый native observer](ux-ui/acceptance/wg-ui-first-attempt-native-2026-10-08.json): собственная пара mm-acceptance-wg-ui-1791472667527, свободные UDP51879/198.18.255.240/30, интерфейсы и пиры созданы только через production формы приложения. Observer не создавал конфигурацию. Оба интерфейса/IP/native key generation и поля peers/endpoint/allowed/keepalive/comment подтвердились, native peer key association и первый двусторонний Ping3/3 прошли. Публичные ключи переданы в форму в памяти, ключи не включены в отчёты. Создание интерфейса наCHR1 выполнено наПК, наCHR2 при320px. [Мобильная форма](ux-ui/screens/wg-ui-create-server2-320-2026-10-08.png) сохранена без ключей и визуально просмотрена; этот снимок второй попытки, не доказательство переключения.
Полный сценарий НЕ завершён: первая попытка не получила PATCH переключения; вторая, mm-acceptance-wg-ui-1791472879650, после создания интерфейсов остановилась на Добавить пира. Playwright зафиксировал unstable→detached DOM menuitem при перерисовке. Callback props пересоздавали columns/inline actions cell, React сбрасывал Base UI menu. Временные browser harness имеют эти ограничения; успешный финальный browser report не создавался и не заявляется.
Локально components/data-grids/wireguard-data-grid.tsx адаптирован по уже принятому GRE образцу: стабильный WireguardActionsCell и типизированный context передают свежие onExport/onAddPeer/onToggle/onDelete/toggleBusy. Данные строки остаются текущими; composition, styles, row key и installed ReUI APIs сохранены. Tests/e2e/wireguard-menu-refresh.spec.ts воспроизвёл исчезнувшее меню на прежнем коде; после исправления меню переживает pending refresh, показывает новое enabled state и вызывает PATCH с актуальным disabled=false. Вместе с wireguard-tables.spec.ts —6/6 passed. ESLint и scoped TypeScript passed. Первый TypeScript запуск совпал с генерацией .next/dev/types и получил временные syntax errors; повтор после остановки Next прошёл. ReUI checklist проверен, общие primitives не менялись.
Первая пара очищена observer, выбранный baseline полностью совпал. Во второй попытке [observer получил401](ux-ui/acceptance/wg-ui-second-observer-error-2026-10-08.json) на завершающем native чтении до cleanup; причина не установлена, пароли не менялись. Повторное чтение через приложение успешно. [Штатный API cleanup](ux-ui/acceptance/wg-ui-second-api-cleanup-2026-10-08.json) проверил точные name/ownership comment и удалил оба own интерфейса DELETE200/200. [Независимая native проверка](ux-ui/acceptance/wg-ui-final-native-cleanup-2026-10-08.json) подтвердила отсутствие own WG/peers/IP/routes на обоих CHR, total IPv4 addresses1/2 и routes4/5; WG interfaces/peers0. Второй baseline был только в памяти и потерян при выходе observer: точное равенство его baseline не заявляется. [DB controls](ux-ui/acceptance/wg-ui-db-controls-2026-10-08.json) servers2/speedHistory9/retention14/migrations9 совпали. Эфемерные контейнеры завершены, остались три основные службы, backend healthy. Audit revisions намеренно сохранены.
Runtime backend/frontend3722fad, локальное меню ещё не развёрнуто. Следующий12.5.62 — frontend Docker deploy и полный UI create interface/peer→disable/enable→повторный handshake/Ping→delete на новой реальной паре, без повторных открытий меню. Перед повтором observer должен сохранить приватный baseline на сервере и продолжать независимую cleanup каждого устройства при ошибке финального state read; запуск browser только после подтверждения готовности нового observer. Переключение, экспорт/import, throughput/RBAC и прочие внешние ограничения остаются открытыми. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
+12
View File
@@ -461,3 +461,15 @@ DELETE200/200, собственные GRE/IP/connected routes удалены; в
Итоговый harness exit0: afterBrowserDelete пуст на обеих сторонах, выбранные WG/peers/IP/routes/IPv4 Firewall поля совпали с baseline. Fallback не понадобился для peers/IP/interfaces; ephemeral контейнер удалён, остались три основные Compose-службы. WireGuard revisions намеренно сохранены как история операций. [Smoke](ux-ui/acceptance/deploy-browser-3722fad-2026-10-08.json): dashboard/servers/uptime/settings/filters/alerts200,320px fit, JS/API errors0.
Приняты успешное обычное удаление own интерфейса с IPv4 адресом и пиром и соответствующий UI результат. Partial failures/read errors/сохранение чужих WG объектов проверены локальными тестами59; на этом стенде такие отказы не инжектировались. IPv6 lifecycle, concurrency/multi-instance, throughput, Telegram/Portal и остальные внешние ограничения остаются открытыми. Следующий12.5.61 — реальные WireGuard действия через UI: создание интерфейса и пира, переключение и очистка собственной пары; ключи не публиковать. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
## 12.5.61 — UI создание WireGuard и локальное исправление нестабильного меню (2026-10-08)
[Первый native observer](ux-ui/acceptance/wg-ui-first-attempt-native-2026-10-08.json): собственная пара mm-acceptance-wg-ui-1791472667527, свободные UDP51879/198.18.255.240/30, интерфейсы и пиры созданы только через production формы приложения. Observer не создавал конфигурацию. Оба интерфейса/IP/native key generation и поля peers/endpoint/allowed/keepalive/comment подтвердились, native peer key association и первый двусторонний Ping3/3 прошли. Публичные ключи переданы в форму в памяти, ключи не включены в отчёты. Создание интерфейса наCHR1 выполнено наПК, наCHR2 при320px. [Мобильная форма](ux-ui/screens/wg-ui-create-server2-320-2026-10-08.png) сохранена без ключей и визуально просмотрена; этот снимок второй попытки, не доказательство переключения.
Полный сценарий НЕ завершён: первая попытка не получила PATCH переключения; вторая, mm-acceptance-wg-ui-1791472879650, после создания интерфейсов остановилась на Добавить пира. Playwright зафиксировал unstable→detached DOM menuitem при перерисовке. Callback props пересоздавали columns/inline actions cell, React сбрасывал Base UI menu. Временные browser harness имеют эти ограничения; успешный финальный browser report не создавался и не заявляется.
Локально components/data-grids/wireguard-data-grid.tsx адаптирован по уже принятому GRE образцу: стабильный WireguardActionsCell и типизированный context передают свежие onExport/onAddPeer/onToggle/onDelete/toggleBusy. Данные строки остаются текущими; composition, styles, row key и installed ReUI APIs сохранены. Tests/e2e/wireguard-menu-refresh.spec.ts воспроизвёл исчезнувшее меню на прежнем коде; после исправления меню переживает pending refresh, показывает новое enabled state и вызывает PATCH с актуальным disabled=false. Вместе с wireguard-tables.spec.ts —6/6 passed. ESLint и scoped TypeScript passed. Первый TypeScript запуск совпал с генерацией .next/dev/types и получил временные syntax errors; повтор после остановки Next прошёл. ReUI checklist проверен, общие primitives не менялись.
Первая пара очищена observer, выбранный baseline полностью совпал. Во второй попытке [observer получил401](ux-ui/acceptance/wg-ui-second-observer-error-2026-10-08.json) на завершающем native чтении до cleanup; причина не установлена, пароли не менялись. Повторное чтение через приложение успешно. [Штатный API cleanup](ux-ui/acceptance/wg-ui-second-api-cleanup-2026-10-08.json) проверил точные name/ownership comment и удалил оба own интерфейса DELETE200/200. [Независимая native проверка](ux-ui/acceptance/wg-ui-final-native-cleanup-2026-10-08.json) подтвердила отсутствие own WG/peers/IP/routes на обоих CHR, total IPv4 addresses1/2 и routes4/5; WG interfaces/peers0. Второй baseline был только в памяти и потерян при выходе observer: точное равенство его baseline не заявляется. [DB controls](ux-ui/acceptance/wg-ui-db-controls-2026-10-08.json) servers2/speedHistory9/retention14/migrations9 совпали. Эфемерные контейнеры завершены, остались три основные службы, backend healthy. Audit revisions намеренно сохранены.
Runtime backend/frontend3722fad, локальное меню ещё не развёрнуто. Следующий12.5.62 — frontend Docker deploy и полный UI create interface/peer→disable/enable→повторный handshake/Ping→delete на новой реальной паре, без повторных открытий меню. Перед повтором observer должен сохранить приватный baseline на сервере и продолжать независимую cleanup каждого устройства при ошибке финального state read; запуск browser только после подтверждения готовности нового observer. Переключение, экспорт/import, throughput/RBAC и прочие внешние ограничения остаются открытыми. Пользовательское правило настоящих VPN объектов сохраняется. Локальный коммит без push.
@@ -0,0 +1,16 @@
{
"date": "2026-10-08",
"before": {
"servers": 2,
"speedHistory": 9,
"retentionDays": 14,
"migrations": 9
},
"after": {
"servers": 2,
"speedHistory": 9,
"retentionDays": 14,
"migrations": 9
},
"unchanged": true
}
@@ -0,0 +1,36 @@
{
"date": "2026-10-08",
"names": [
"mm-acceptance-wg-ui-1791472667527",
"mm-acceptance-wg-ui-1791472879650"
],
"results": [
{
"serverId": 1,
"ownInterfaces": 0,
"ownPeers": 0,
"ownAddresses": 0,
"ownRoutes": 0,
"totalInterfaces": 0,
"totalPeers": 0,
"totalAddresses": 1,
"totalRoutes": 4
},
{
"serverId": 2,
"ownInterfaces": 0,
"ownPeers": 0,
"ownAddresses": 0,
"ownRoutes": 0,
"totalInterfaces": 0,
"totalPeers": 0,
"totalAddresses": 2,
"totalRoutes": 5
}
],
"keysOmitted": true,
"limits": [
"Independent final absence check; original baseline of second attempt lost after observer exit",
"First attempt full selected baseline was verified"
]
}
@@ -0,0 +1,199 @@
{
"date": "2026-10-08",
"backendSource": "3722fad",
"frontendSource": "3722fad",
"name": "mm-acceptance-wg-ui-1791472667527",
"listenPort": 51879,
"subnet": "198.18.255.240/30",
"realRouterOs": true,
"pings": [
{
"round": 1,
"results": [
{
"serverId": 1,
"received": 3,
"summary": {
"sent": "3",
"received": "3",
"packet-loss": "0",
"avg-rtt": "55ms80us"
}
},
{
"serverId": 2,
"received": 3,
"summary": {
"sent": "3",
"received": "3",
"packet-loss": "0",
"avg-rtt": "55ms40us"
}
}
],
"passed": true
}
],
"cleanup": [
{
"serverId": 1,
"/interface/wireguard": true,
"/interface/wireguard/peers": true,
"/ip/address": true,
"/ip/route": true,
"/ip/firewall/filter": true,
"/ip/firewall/nat": true,
"/ip/firewall/mangle": true
},
{
"serverId": 2,
"/interface/wireguard": true,
"/interface/wireguard/peers": true,
"/ip/address": true,
"/ip/route": true,
"/ip/firewall/filter": true,
"/ip/firewall/nat": true,
"/ip/firewall/mangle": true
}
],
"uiCreatesOnly": true,
"nativePeerAssociationConfirmed": true,
"connectedState": [
{
"serverId": 1,
"host": "194.226.121.150",
"expectedAddress": "198.18.255.241/30",
"interface": {
"rosId": "*9",
"appId": "1:*9",
"name": "mm-acceptance-wg-ui-1791472667527",
"listenPort": "51879",
"mtu": "1420",
"disabled": "false",
"running": "true",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"keyGenerated": true
},
"addresses": [
"198.18.255.241/30"
],
"peers": [
{
"rosId": "*5",
"allowedAddresses": "198.18.255.242/32",
"endpoint": "104.165.244.7:51879",
"keepalive": "5s",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"disabled": "false",
"handshake": "2s",
"rx": "756",
"tx": "848"
}
]
},
{
"serverId": 2,
"host": "104.165.244.7",
"expectedAddress": "198.18.255.242/30",
"interface": {
"rosId": "*5",
"appId": "2:*5",
"name": "mm-acceptance-wg-ui-1791472667527",
"listenPort": "51879",
"mtu": "1420",
"disabled": "false",
"running": "true",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"keyGenerated": true
},
"addresses": [
"198.18.255.242/30"
],
"peers": [
{
"rosId": "*3",
"allowedAddresses": "198.18.255.241/32",
"endpoint": "194.226.121.150:51879",
"keepalive": "5s",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"disabled": "false",
"handshake": "2s",
"rx": "700",
"tx": "756"
}
]
}
],
"afterBrowser": [
{
"serverId": 1,
"host": "194.226.121.150",
"expectedAddress": "198.18.255.241/30",
"interface": {
"rosId": "*9",
"appId": "1:*9",
"name": "mm-acceptance-wg-ui-1791472667527",
"listenPort": "51879",
"mtu": "1420",
"disabled": "false",
"running": "true",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"keyGenerated": true
},
"addresses": [
"198.18.255.241/30"
],
"peers": [
{
"rosId": "*5",
"allowedAddresses": "198.18.255.242/32",
"endpoint": "104.165.244.7:51879",
"keepalive": "5s",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"disabled": "false",
"handshake": "1m54s",
"rx": "1552",
"tx": "1028"
}
]
},
{
"serverId": 2,
"host": "104.165.244.7",
"expectedAddress": "198.18.255.242/30",
"interface": {
"rosId": "*5",
"appId": "2:*5",
"name": "mm-acceptance-wg-ui-1791472667527",
"listenPort": "51879",
"mtu": "1420",
"disabled": "false",
"running": "true",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"keyGenerated": true
},
"addresses": [
"198.18.255.242/30"
],
"peers": [
{
"rosId": "*3",
"allowedAddresses": "198.18.255.241/32",
"endpoint": "194.226.121.150:51879",
"keepalive": "5s",
"comment": "Temporary UX acceptance mm-acceptance-wg-ui-1791472667527",
"disabled": "false",
"handshake": "1m54s",
"rx": "880",
"tx": "1552"
}
]
}
],
"limits": [
"Only own WireGuard resources; no default route or firewall changes",
"Public keys transferred to browser only in memory; no keys persisted in report",
"Short handshake/ICMP, not throughput or failure/RBAC acceptance",
"WireGuard revision audit retained"
]
}
@@ -0,0 +1,19 @@
{
"date": "2026-10-08",
"name": "mm-acceptance-wg-ui-1791472879650",
"results": [
{
"serverId": "1",
"status": 200
},
{
"serverId": "2",
"status": 200
}
],
"applicationCatalogAbsent": true,
"limits": [
"Cleanup recovered after transient observer 401; separate native reads verify no own remnants",
"Only exact name/ownership comment fixtures deleted"
]
}
@@ -0,0 +1,17 @@
{
"date": "2026-10-08",
"name": "mm-acceptance-wg-ui-1791472879650",
"observerError": {
"status": 401,
"path": "/interface/wireguard",
"stage": "final native read before cleanup"
},
"cleanupSkippedByObserver": true,
"recoveredViaApplicationDelete": true,
"finalNativeOwnObjectsAbsent": true,
"limits": [
"Cause of transient 401 not established; later app/native reads succeeded",
"Passwords were not changed",
"Second in-memory baseline was lost; no claim of exact second baseline equality"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+35
View File
@@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test'
test('WireGuard: меню переживает refresh и использует новое состояние интерфейса', async ({ page }) => {
let refreshing = false, release: (() => void) | undefined, patches = 0
const gate = new Promise<void>(resolve => { release = resolve })
const iface = { id: '1:*A', rosId: '*A', name: 'wg-menu', serverId: '1', serverName: 'CHR', listenPort: 51879, mtu: 1420, comment: '', enabled: true, status: 'up', peers: [] }
await page.route('**/health', route => route.fulfill({ json: { ok: true } }))
await page.route('**/api/**', async route => {
const path = new URL(route.request().url()).pathname
if (route.request().method() === 'PATCH') {
expect(path).toBe('/api/wireguard/interfaces/1/*A')
expect(route.request().postDataJSON()).toEqual({ disabled: false })
patches++
return route.fulfill({ json: { ok: true } })
}
if (path === '/api/wireguard') {
if (refreshing) await gate
return route.fulfill({ json: { interfaces: [{ ...iface, enabled: !refreshing, status: refreshing ? 'down' : 'up' }] } })
}
return route.fulfill({ json: path === '/api/auth/config' ? { required: false } : path === '/api/servers' ? [{ id: 1, name: 'CHR', host: '192.0.2.1', enabled: true, type: 'home-router', country: 'RU', status: 'online' }] : {} })
})
await page.goto('/wireguard')
const refresh = page.getByRole('button', { name: 'Обновить', exact: true })
await expect(refresh).toBeEnabled()
await page.getByRole('button', { name: 'Действия: wg-menu' }).click()
await expect(page.getByRole('menuitem', { name: 'Отключить', exact: true })).toBeVisible()
refreshing = true
await refresh.evaluate(e => (e as HTMLButtonElement).click())
await expect(refresh).toBeDisabled()
await expect(page.getByRole('menuitem', { name: 'Добавить пира', exact: true })).toBeVisible()
release!()
await expect(refresh).toBeEnabled()
await expect(page.getByRole('menuitem', { name: 'Включить', exact: true })).toBeVisible()
await page.getByRole('menuitem', { name: 'Включить', exact: true }).click()
await expect.poll(() => patches).toBe(1)
})