From efc6da1249b7b6c14bfd299c7b9af5bf9ced999a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 28 Sep 2026 23:58:59 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B0=20WireGuard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/wireguard/page.tsx | 4 ++++ docs/ux-ui-plan.md | 4 ++-- docs/ux-ui-progress.md | 9 ++++++++- docs/ux-ui-stage-9.md | 6 +++++- shared/api/wireguard.ts | 10 +++++++++- tests/e2e/wireguard-links.spec.ts | 32 +++++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/app/(main)/wireguard/page.tsx b/app/(main)/wireguard/page.tsx index c128205d..df497d5e 100644 --- a/app/(main)/wireguard/page.tsx +++ b/app/(main)/wireguard/page.tsx @@ -455,6 +455,10 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf format: args.format, dryRun: args.dryRun, }) + if (res.dryRun) { + toast.info("Конфигурация проверена без применения на роутер") + return + } toast.success( res.applied ? `Импортировано: ${res.applied.interfaceName} (+${res.applied.peersCreated} пиров)` diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index a2cc9ff6..7c363a54 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -80,7 +80,7 @@ flowchart LR ## Текущая точка продолжения -По журналу этапы 0–8 завершены в границах локальной приёмки; внешние ограничения сохраняются. Этап 9 начат (блок 9.1 — удаление WireGuard); этапы 10–12 не начаты. Этап 11 разделён: 11А — пользователи, контейнеры, сертификаты, резервирование; 11Б — оповещения, сбор данных, интеграции, релизы, настройки. +По журналу этапы 0–8 завершены в границах локальной приёмки; внешние ограничения сохраняются. Этап 9 начат (блоки 9.1–9.2 — удаление и подтверждение импорта WireGuard); этапы 10–12 не начаты. Этап 11 разделён: 11А — пользователи, контейнеры, сертификаты, резервирование; 11Б — оповещения, сбор данных, интеграции, релизы, настройки. Подробные планы: [этап 6](ux-ui-stage-6.md), [этап 7](ux-ui-stage-7.md), [этап 8 и итоговая матрица](ux-ui-stage-8.md), [этап 9](ux-ui-stage-9.md). @@ -88,7 +88,7 @@ flowchart LR Ближайший оставшийся объём: -1. Этап 9: карта разделов и схема сохранены; удаление WireGuard исправлено и проверено в 9.1. Далее — создание/импорт/добавление пира: контекст формы, ошибка и повтор, различие dry-run и применения; затем IPsec/GRE/VXLAN/Firewall/Filters. +1. Этап 9: карта разделов и схема сохранены; удаление и подтверждение импорта WireGuard исправлены и проверены в 9.1–9.2. Далее — создание/импорт/добавление пира: контекст формы, ошибка и повтор, различие dry-run и применения; затем IPsec/GRE/VXLAN/Firewall/Filters. 2. Сохранять контекст устройства, введённые значения при ошибке и подтверждать результат операций по API; локальные моки проверяют клиентские сценарии, реальные сетевые операции требуют тестового окружения. 3. В этапе 12 выполнить полный аудит доступности (скринридер, контраст), производительности и сквозную регрессию. Ограничения этапа 8 — удалённые серверы месячного каталога, отсутствие экспорта и автоматического переподключения SSE — остаются явно зафиксированными, без заявления о реализации. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 09ea44cf..3884e23e 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -25,12 +25,19 @@ | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | -| 9 | VPN, Firewall и Filters | В работе: блок 9.1 — подтверждение удаления WireGuard | +| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.2 — подтверждение удаления и импорта WireGuard | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 9.2 — подтверждение применения импорта WireGuard (2026-09-28) + +- По прямому поручению пользователя продолжена работа сверх резерва 25%. Взята ограниченная проверка импорта без расширения форм и сетевых правил. +- Ответ импорта проверяется существующей схемой contracts; dryRun должен соответствовать запросу, применение требует applied. Повреждённый ответ, dry-run вместо применения и отсутствие applied больше не объявляются успешным импортом. Корректный dry-run в обработчике обозначен как проверка без применения и не закрывает форму; отдельной серверной кнопки dry-run сейчас нет, «Предпросмотр» разбирает конфигурацию локально. +- Новый E2E прошёл: 403, dry-run, отсутствие applied, пустой ответ и успешное применение; после ошибок сохраняются текст и выбранный сервер. Итоговые lint и TypeScript прошли. Общая регрессия/production build не повторялись; реальные RouterOS/JWT не проверены. +- Далее: создание/добавление пира, блокировка повторных операций и закрытия формы импорта во время запроса. Этот блок не закрывает весь UX импорта. На контрольной точке использовано 97% пятичасового и 46% недельного лимита. Локальный коммит без push. + ## Блок 9.1 — подтверждение удаления WireGuard (2026-09-28) - Сохранена [карта этапа 9 со схемой и референсами](ux-ui-stage-9.md). Первичная инвентаризация API и контекста выполнена; полный аудит остальных операций впереди. diff --git a/docs/ux-ui-stage-9.md b/docs/ux-ui-stage-9.md index 5ffbac33..6c70a705 100644 --- a/docs/ux-ui-stage-9.md +++ b/docs/ux-ui-stage-9.md @@ -1,6 +1,6 @@ # Этап 9: VPN и сетевые операции -Статус: начат 2026-09-28, выполнен блок 9.1. Основа — существующие ReUI PRO/base-nova, Frame, ServerTileRail, DataGrid и формы. Правила применения сетевой конфигурации не меняем. +Статус: начат 2026-09-28, выполнены блоки 9.1–9.2. Основа — существующие ReUI PRO/base-nova, Frame, ServerTileRail, DataGrid и формы. Правила применения сетевой конфигурации не меняем. ## Схема и референсы принятого плана @@ -45,3 +45,7 @@ - Сохранён установленный AlertDialog/Base UI, без изменения темы/registry. Получен ReUI audit checklist; изменены поведение и доступные состояния. Далее: создание/импорт/добавление пира WireGuard — сохранение формы, повторные операции, различие dry-run и применения. Затем остальные разделы по карте. Реальные RouterOS/JWT и сетевые последствия требуют отдельного окружения. Этап 9 не завершён. + +## Блок 9.2 — достоверность ответа импорта + +Ответ проверяется схемой contracts: режим dryRun соответствует запросу, применение подтверждается applied. Ошибка сохраняет текст и сервер. Один E2E с пятью последовательными ответами (403, dry-run, отсутствие applied, повреждённый ответ, успех), lint и TypeScript прошли. Серверный dry-run не добавлен в UI; предпросмотр остаётся локальным. Далее — создание/пиры, защита повторов и закрытия формы импорта во время запроса. Реальные сетевые операции не проверены. diff --git a/shared/api/wireguard.ts b/shared/api/wireguard.ts index 229b5e64..9335a5f2 100644 --- a/shared/api/wireguard.ts +++ b/shared/api/wireguard.ts @@ -1,3 +1,4 @@ +import { wgImportResponseSchema } from "@mmapp/contracts/wireguard" import type { WgCreateInterface, WgCreatePeerRequest, @@ -90,10 +91,17 @@ export async function importWireGuard( baseUrl: string, payload: WgImportRequest, ): Promise { - return requestJson(baseUrl, "/api/wireguard/import", { + const response = await requestJson(baseUrl, "/api/wireguard/import", { method: "POST", body: JSON.stringify(payload), }) + const parsed = wgImportResponseSchema.safeParse(response) + if (!parsed.success) throw new Error("Некорректный ответ импорта WireGuard") + const result = parsed.data + if (result.dryRun !== (payload.dryRun ?? false) || (!result.dryRun && !result.applied)) { + throw new Error("Сервер не подтвердил применение конфигурации") + } + return result } export async function exportWireGuard( diff --git a/tests/e2e/wireguard-links.spec.ts b/tests/e2e/wireguard-links.spec.ts index e394de70..3ecd27b0 100644 --- a/tests/e2e/wireguard-links.spec.ts +++ b/tests/e2e/wireguard-links.spec.ts @@ -175,3 +175,35 @@ for (const kind of ["iface", "peer"] as const) { expect(attempts).toBe(3) }) } + +test("WireGuard: импорт сохраняет форму без подтверждения применения", async ({ page }) => { + await setup(page, "/wireguard?server=1") + let attempts = 0 + const preview = { format: "rsc", interface: { name: "wg-import-test" }, peers: [] } + const content = "/interface wireguard add name=wg-import-test listen-port=13231" + await page.route("**/api/wireguard/import", async route => { + expect(route.request().postDataJSON()).toEqual({ serverId: "1", content, format: "auto", dryRun: false }) + attempts++ + if (attempts === 1) return route.fulfill({ status: 403, json: { error: "Нет доступа к импорту" } }) + if (attempts === 2) return route.fulfill({ json: { dryRun: true, preview } }) + if (attempts === 3) return route.fulfill({ json: { dryRun: false, preview } }) + if (attempts === 4) return route.fulfill({ json: {} }) + return route.fulfill({ json: { dryRun: false, preview, applied: { interfaceName: "wg-import-test", peersCreated: 0 } } }) + }) + await page.getByRole("button", { name: "Импорт", exact: true }).click() + const dialog = page.getByRole("dialog", { name: "Импорт конфига WireGuard" }) + await dialog.locator("textarea").fill(content) + for (const message of ["Нет доступа к импорту", "Сервер не подтвердил применение конфигурации", "Сервер не подтвердил применение конфигурации", "Некорректный ответ импорта WireGuard"]) { + const before = attempts + await dialog.getByRole("button", { name: "Применить на роутер" }).click() + await expect.poll(() => attempts).toBe(before + 1) + await expect(page.getByText(message, { exact: true }).first()).toBeVisible() + await expect(dialog.locator("textarea")).toHaveValue(content) + await expect(dialog.locator("select").first()).toHaveValue("1") + await expect(dialog.getByRole("button", { name: "Применить на роутер" })).toBeEnabled() + } + await dialog.getByRole("button", { name: "Применить на роутер" }).click() + await expect(dialog).toHaveCount(0) + await expect(page.getByText("Импортировано: wg-import-test (+0 пиров)", { exact: true })).toBeVisible() + expect(attempts).toBe(5) +})