diff --git a/components/ipsec/ipsec-users-grid.tsx b/components/ipsec/ipsec-users-grid.tsx index c5ac39e7..7ba6b314 100644 --- a/components/ipsec/ipsec-users-grid.tsx +++ b/components/ipsec/ipsec-users-grid.tsx @@ -67,6 +67,7 @@ function IpsecUsersGrid({ type="button" disabled={!onEdit || (canManage && !canManage(c.serverId))} className="truncate text-left font-medium hover:underline" + title={c.name} onClick={() => onEdit?.(c)} > {c.name} diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index f7e6d8c0..9ce85946 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -101,7 +101,13 @@ flowchart LR Снимки HTTP-фикстур просмотрены; в блоке 9.47 прошли общая регрессия Filters (33 E2E) и production build frontend (34 страницы). Этап 9 остаётся в работе; следующие блоки — ограничения IPsec/VXLAN и общего GRE-каталога по журналу. -В блоке 9.48 исправлены первоначальные и частичные ошибки чтения IPsec; далее readiness действий, свежесть и гонки этого раздела. +В блоках 9.48–9.52 исправлены чтение, readiness, свежесть и контекст IPsec; прошли профильные операции, ПК/телефон и frontend build. Этап 9 открыт; далее VXLAN/GRE. + +| IPsec — проверенные экраны | ПК | Телефон | +| --- | --- | --- | +| Устройство → свежесть → клиенты | [Экран](ux-ui/screens/ipsec-desktop.png) | [Экран](ux-ui/screens/ipsec-mobile.png) | +| Клиент → форма → подтверждение API | [Форма](ux-ui/screens/ipsec-client-desktop.png) | [Форма](ux-ui/screens/ipsec-client-mobile.png) | +| Устройство → peer/сертификаты → действия | [Серверы](ux-ui/screens/ipsec-servers-desktop.png) | [Сервер](ux-ui/screens/ipsec-servers-mobile.png) | ## Порядок продолжения и журналирования diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index b5996753..22f94bb0 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -25,12 +25,25 @@ | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | -| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.51 — IPsec/GRE/VXLAN, Firewall и операции Filters | +| 9 | VPN, Firewall и Filters | В работе: блоки 9.1–9.52 — IPsec/GRE/VXLAN, Firewall и операции Filters | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 9.52 — клавиатура и экраны IPsec (2026-09-30) + +- Два E2E проверили ПК 1440×1000 и телефон 390×844: выбор устройства на телефоне, открытие клиента клавиатурой, сохранение через Enter с единственным PATCH в правильный serverId, отсутствие горизонтального переполнения документа, границы формы и серверные действия. Шесть PNG сохранены и просмотрены. Имена клиентов получают title для обрезанного текста. +- Первые прогоны уточнили существующее имя поля и дождались штатной анимации Sheet; оба исправленных сценария прошли. Снимок сервера телефона показывает прокрученный контент с кнопками; таблица имеет внутреннюю горизонтальную прокрутку. Это профильная проверка PSK-редактирования, не вся приёмка screen reader/контраста/всех полей. +- Production build frontend прошёл (контракты/TypeScript, 34 страницы), lint изменённого компонента/теста чистый. Backend не менялся, последний build — 9.44. Снимки используют HTTP-фикстуры, реальные RouterOS/JWT/БД не проверены. +- Далее — оставшиеся ограничения VXLAN: достоверность после общего сбоя, свежесть, режим просмотра и источник; затем ошибки общего GRE-каталога. Этап 9 открыт. Локальный коммит без push. + +| Референс | ПК | Телефон | +| --- | --- | --- | +| [Data Grid Filtering 2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | [Клиенты](ux-ui/screens/ipsec-desktop.png) | [Клиенты](ux-ui/screens/ipsec-mobile.png) | +| [Sheet 8](https://reui.io/preview/base/sheet-8?ref=mcp) | [Форма клиента](ux-ui/screens/ipsec-client-desktop.png) | [Форма клиента](ux-ui/screens/ipsec-client-mobile.png) | +| [List 9](https://reui.io/preview/base/list-9?ref=mcp) | [Серверы](ux-ui/screens/ipsec-servers-desktop.png) | [Сервер](ux-ui/screens/ipsec-servers-mobile.png) | + ## Блок 9.51 — свежесть IPsec (2026-09-30) - По выбранному устройству показано клиентское время получения успешного ответа; по всем устройствам — время последнего валидного ответа IPsec, которое не означает успешный опрос каждого роутера. Частичный отказ сохраняет прошлое время неответившего устройства, полный отказ не меняет ни одну дату. До первого успешного результата время неизвестно; время отмечается только для устройств, присутствующих в успешных сводках/клиентах. diff --git a/docs/ux-ui-stage-9.md b/docs/ux-ui-stage-9.md index 969cb5eb..20c4a92a 100644 --- a/docs/ux-ui-stage-9.md +++ b/docs/ux-ui-stage-9.md @@ -271,3 +271,16 @@ Live-копирование читает актуальный целевой rul - Подтверждённые изменения/восстановление показывают отдельный timestamp API с serverId. Он не заменяет последующий опрос. ISO доступен через time.dateTime, title поясняет клиентские часы; даты не сохраняются между перезагрузками, возраст не обновляется таймером. - 8 E2E прошли: данные/схемы, новый тест управляемых часов с выбором устройства и расширенный замок удаления/ошибка опроса с подтверждением записи. TypeScript и lint чистые. Backend не менялся, build пока не повторялся. - Далее — профильная клавиатурная и адаптивная приёмка IPsec, проверенные экраны в плане; затем оставшиеся VXLAN/GRE. Реальные RouterOS/JWT/БД не проверены. Локальный коммит без push. + +## Блок 9.52 — клавиатура и экраны IPsec (2026-09-30) + +- Два E2E проверили ПК 1440×1000 и телефон 390×844: выбор устройства на телефоне, открытие клиента клавиатурой, сохранение через Enter с единственным PATCH в правильный serverId, отсутствие горизонтального переполнения документа, границы формы и серверные действия. Шесть PNG сохранены и просмотрены. Имена клиентов получают title для обрезанного текста. +- Первые прогоны уточнили существующее имя поля и дождались штатной анимации Sheet; оба исправленных сценария прошли. Снимок сервера телефона показывает прокрученный контент с кнопками; таблица имеет внутреннюю горизонтальную прокрутку. Это профильная проверка PSK-редактирования, не вся приёмка screen reader/контраста/всех полей. +- Production build frontend прошёл (контракты/TypeScript, 34 страницы), lint изменённого компонента/теста чистый. Backend не менялся, последний build — 9.44. Снимки используют HTTP-фикстуры, реальные RouterOS/JWT/БД не проверены. +- Далее — оставшиеся ограничения VXLAN: достоверность после общего сбоя, свежесть, режим просмотра и источник; затем ошибки общего GRE-каталога. Этап 9 открыт. Локальный коммит без push. + +| Референс | ПК | Телефон | +| --- | --- | --- | +| [Data Grid Filtering 2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | [Клиенты](ux-ui/screens/ipsec-desktop.png) | [Клиенты](ux-ui/screens/ipsec-mobile.png) | +| [Sheet 8](https://reui.io/preview/base/sheet-8?ref=mcp) | [Форма клиента](ux-ui/screens/ipsec-client-desktop.png) | [Форма клиента](ux-ui/screens/ipsec-client-mobile.png) | +| [List 9](https://reui.io/preview/base/list-9?ref=mcp) | [Серверы](ux-ui/screens/ipsec-servers-desktop.png) | [Сервер](ux-ui/screens/ipsec-servers-mobile.png) | diff --git a/docs/ux-ui/screens/ipsec-client-desktop.png b/docs/ux-ui/screens/ipsec-client-desktop.png new file mode 100644 index 00000000..c4985365 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-client-desktop.png differ diff --git a/docs/ux-ui/screens/ipsec-client-mobile.png b/docs/ux-ui/screens/ipsec-client-mobile.png new file mode 100644 index 00000000..832f0ff7 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-client-mobile.png differ diff --git a/docs/ux-ui/screens/ipsec-desktop.png b/docs/ux-ui/screens/ipsec-desktop.png new file mode 100644 index 00000000..2453cc7a Binary files /dev/null and b/docs/ux-ui/screens/ipsec-desktop.png differ diff --git a/docs/ux-ui/screens/ipsec-mobile.png b/docs/ux-ui/screens/ipsec-mobile.png new file mode 100644 index 00000000..3b84ee73 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-mobile.png differ diff --git a/docs/ux-ui/screens/ipsec-servers-desktop.png b/docs/ux-ui/screens/ipsec-servers-desktop.png new file mode 100644 index 00000000..2d2b5da1 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-servers-desktop.png differ diff --git a/docs/ux-ui/screens/ipsec-servers-mobile.png b/docs/ux-ui/screens/ipsec-servers-mobile.png new file mode 100644 index 00000000..5972d82f Binary files /dev/null and b/docs/ux-ui/screens/ipsec-servers-mobile.png differ diff --git a/tests/e2e/ipsec-layout.spec.ts b/tests/e2e/ipsec-layout.spec.ts new file mode 100644 index 00000000..07c2a774 --- /dev/null +++ b/tests/e2e/ipsec-layout.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from "@playwright/test" +import { mkdirSync } from "node:fs" + +for (const device of ["desktop", "mobile"] as const) { + test(`IPsec: клавиатура, контекст и layout ${device}`, async ({ page }) => { + await page.setViewportSize(device === "desktop" ? { width: 1440, height: 1000 } : { width: 390, height: 844 }) + await page.clock.install({ time: new Date("2026-09-30T05:00:00Z") }) + const devices = [1, 2].map(id => ({ id, name: id === 1 ? "Основной VPN роутер" : "Резервный VPN роутер", host: "192.0.2." + id, country: "RU", enabled: true, type: "exit-node", status: "online" })) + const clients = ["1", "2"].map(id => ({ id: id + "-*1", rosId: "*1", serverId: id, serverName: devices[Number(id) - 1].name, name: id === "1" ? "Ноутбук инженера" : "Резервный клиент", authMethod: "pre-shared-key", kind: "psk", remoteId: "engineer", peerName: "ikev2-vpn", online: false, disabled: false, managed: true })) + const summaries = ["1", "2"].map(id => ({ serverId: id, serverName: devices[Number(id) - 1].name, initialized: true, ike2Ready: true, serverEndpoint: "vpn.example.test", caCert: { name: "vpn-ca", role: "ca", managed: true, trusted: true }, natRuleManaged: true, clientsTotal: 1, clientsOnline: 0, peers: [{ id: id + "-*p1", rosId: "*p1", serverId: id, serverName: devices[Number(id) - 1].name, name: "ikev2-vpn", passive: true, exchangeMode: "ike2", profile: "vpn-profile", address: "0.0.0.0/0", disabled: false, managed: true }], certs: [{ name: "vpn-ca", role: "ca", managed: true, trusted: true }] })) + let writes = 0 + 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() === "PATCH") { + writes++ + expect(path).toContain(device === "mobile" ? "/users/2/" : "/users/1/") + expect(r.request().postDataJSON()).toMatchObject({ name: "Проверенный клиент" }) + clients[device === "mobile" ? 1 : 0].name = "Проверенный клиент" + return r.fulfill({ json: { ok: true } }) + } + return r.fulfill({ json: path === "/api/ipsec" ? { servers: summaries, clients } : path === "/api/servers" ? devices : path === "/api/auth/config" ? { required: false } : {} }) + }) + await page.goto("/ipsec") + await expect(page.locator("tr[data-row-id]")).toHaveCount(2) + await expect(page.getByRole("button", { name: "Новый клиент", exact: true })).toBeEnabled() + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }) + mkdirSync("docs/ux-ui/screens", { recursive: true }) + await page.screenshot({ path: `docs/ux-ui/screens/ipsec-${device}.png`, animations: "disabled" }) + if (device === "mobile") { + await page.getByRole("button", { name: "Серверы", exact: true }).click() + await page.getByRole("dialog").getByRole("option").filter({ hasText: "Резервный VPN роутер" }).click() + await expect(page.getByRole("dialog")).toHaveCount(0) + await expect(page.locator("tr[data-row-id]")).toHaveCount(1) + } + const edit = page.getByRole("button", { name: device === "mobile" ? "Резервный клиент" : "Ноутбук инженера", exact: true }) + await edit.focus() + await page.keyboard.press("Enter") + const dialog = page.getByRole("dialog") + await dialog.getByRole("textbox", { name: "Имя клиента" }).fill("Проверенный клиент") + await expect.poll(() => dialog.evaluate(el => el.getBoundingClientRect().left >= 0 && el.getBoundingClientRect().right <= innerWidth)).toBe(true) + await page.screenshot({ path: `docs/ux-ui/screens/ipsec-client-${device}.png`, animations: "disabled" }) + await dialog.getByRole("button", { name: "Сохранить", exact: true }).focus() + await page.keyboard.press("Enter") + await expect(dialog).toHaveCount(0) + await expect(page.locator("tr[data-row-id]").filter({ hasText: "Проверенный клиент" })).toHaveCount(1) + expect(writes).toBe(1) + await page.getByRole("tab", { name: "Сервер", exact: true }).click() + await expect(page.getByRole("button", { name: "Переинициализировать", exact: true }).first()).toBeEnabled() + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.clock.fastForward(6000) + await expect(page.getByText("Клиент обновлён", { exact: true })).toHaveCount(0) + if (device === "mobile") await page.getByRole("button", { name: "Переинициализировать", exact: true }).scrollIntoViewIfNeeded() + await page.screenshot({ path: `docs/ux-ui/screens/ipsec-servers-${device}.png`, animations: "disabled" }) + }) +}