diff --git a/app/(main)/ipsec/page.tsx b/app/(main)/ipsec/page.tsx index d4017697..daabf731 100644 --- a/app/(main)/ipsec/page.tsx +++ b/app/(main)/ipsec/page.tsx @@ -614,8 +614,9 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor case "server": return "Удалит managed-объекты (identity, mode-config, peer, пул, NAT) и сертификаты IKEv2 на этом роутере. Действие можно откатить через «Историю» (кроме сертификатов)." case "user": - if (deletingCertificateIdentity && !shouldRemoveClientCertificate) return "Удалит только выбранную identity и её неиспользуемый персональный mode-config. Сертификат останется на роутере; другие identity сохранятся." - return "Удалит identity, персональный mode-config и клиентский сертификат на роутере." + if (pendingDelete.client.kind === "psk") return "Удалит выбранную PSK identity и неиспользуемый персональный mode-config приложения. Сертификаты останутся на роутере." + if (deletingCertificateIdentity && !shouldRemoveClientCertificate) return "Удалит выбранную identity и неиспользуемый персональный mode-config приложения. Сертификат останется на роутере; другие identity сохранятся." + return "Удалит identity, неиспользуемый персональный mode-config приложения и прямо привязанный клиентский сертификат на роутере." case "peer": return "Удалит peer на роутере. Если на него ссылаются identity — потребуется принудительное удаление." case "cert": diff --git a/backend/src/routes/ipsec-mutation.test.ts b/backend/src/routes/ipsec-mutation.test.ts index 35cf43f8..ab20e8ae 100644 --- a/backend/src/routes/ipsec-mutation.test.ts +++ b/backend/src/routes/ipsec-mutation.test.ts @@ -6,9 +6,9 @@ import { MikrotikClient, MikrotikError } from "../services/mikrotik.js" import { fetchIpsecState } from "../services/ipsec-live.js" const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters[0] const original = MikrotikClient.fromServer -let writes = 0, records = 0, fault = "", certificateFixture = false +let writes = 0, records = 0, fault = "", certificateFixture = false, removedCertificates = 0 const identities: Record[] = [{ ".id": "*I", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:alice", "mode-config": "mc-ipsec-alice", comment: "MikrotikManager:ipsec user=Alice" }, { ".id": "*OTHER", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:other", "mode-config": "mc-ipsec-other", comment: "MikrotikManager:ipsec user=Other" }] -MikrotikClient.fromServer = () => ({ get: async (p: string) => certificateFixture && p === "/certificate" ? [{ ".id": "*CERT", name: "ipsec-user-shared", "common-name": "shared", "key-usage": "tls-client" }] : p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient +MikrotikClient.fromServer = () => ({ get: async (p: string) => certificateFixture && p === "/certificate" ? [{ ".id": "*CERT", name: "ipsec-user-shared", "common-name": "shared", "key-usage": "tls-client" }] : p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], removeCertificate: async () => { writes++; removedCertificates++ }, put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler) await app.register(createIpsecRoutes({ findServer: async () => server, recordRevision: async () => { records++; return {created:false,revision:{} as never} } }), { prefix: "/api" }) const patch = (payload: Record) => app.inject({ method: "PATCH", url: "/api/ipsec/users/1/identity:*I", payload }) @@ -18,6 +18,16 @@ writes=records=0; fault="partial"; let r=await patch({name:"New",psk:"new-test-s writes=records=0; fault="lost-ack"; r=await patch({name:"Unknown"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"unknown"); assert.equal(r.json().confirmedWrites,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Unknown"); assert.equal(records,0) writes=records=0; fault=""; r=await patch({remoteId:"fresh"}); assert.equal(r.statusCode,200); assert.equal(identities[0]!["remote-id"],"key-id:fresh"); assert.equal(writes,1); assert.equal(records,1) + + +// A same-named certificate is a separate object, not a binding inferred from a PSK label. +certificateFixture=true;identities.splice(0,identities.length,{".id":"*UNBOUND",peer:"ike","auth-method":"pre-shared-key","remote-id":"key-id:unbound",comment:"MikrotikManager:ipsec user=shared"}); +writes=records=removedCertificates=0; +let deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*UNBOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,1);assert.equal(removedCertificates,0);assert.equal(records,1); +// Explicit certificate binding still permits deletion when no other references exist. +identities.splice(0,identities.length,{".id":"*BOUND",peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=shared"}); +writes=records=removedCertificates=0; +deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*BOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,2);assert.equal(removedCertificates,1);assert.equal(records,1); certificateFixture=true;identities.splice(0,identities.length,...["*FIRST","*SECOND"].map((id,n)=>({".id":id,peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=Shared"+n}))); for(const method of ["PATCH","DELETE"] as const){writes=records=0;const r=await app.inject({method,url:"/api/ipsec/users/1/cert:*CERT",...(method==="PATCH"?{payload:{name:"Wrong"}}:{})});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0)} writes=records=0;r=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*FIRST"});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0); diff --git a/backend/src/routes/ipsec.ts b/backend/src/routes/ipsec.ts index 2de345db..38c99c25 100644 --- a/backend/src/routes/ipsec.ts +++ b/backend/src/routes/ipsec.ts @@ -535,10 +535,6 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs const identity = target.identity ?? (target.certRow ? state.identities.find(i => i["remote-certificate"] === target.certRow?.name) : undefined) const certRow = identity ? undefined : target.certRow if (!identity && !certRow) return reply.status(404).send({ error: "Клиент не найден" }) - const managed = identity - ? isIpsecManagedComment(identity.comment) - : String(certRow?.name ?? "").startsWith("ipsec-user-") - const userName = identity ? (parseIpsecUserComment(identity.comment) ?? "") : "" const sharedMcName = (state.modeConfigs.find((m) => (m.name ?? "").trim() === IPSEC_COMMON_NAME)?.name ?? "").trim() const personal = identity ? (identity["mode-config"] ?? "").trim() : "" const remoteCert = identity @@ -546,7 +542,8 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs : String(certRow?.name ?? "").trim() // Refuse before deleting the identity: a shared certificate must survive. - const deletionCertName = remoteCert || (managed && userName ? clientCertName(userName) : "") + // A display name does not establish ownership of a certificate. + const deletionCertName = remoteCert if (removeCertificate && deletionCertName && ( state.identities.some(i => i[".id"] !== identity?.[".id"] && ((i["remote-certificate"] ?? "").trim() === deletionCertName || (i.certificate ?? "").trim() === deletionCertName)) || state.peers.some(p => (p.certificate ?? "").trim() === deletionCertName) @@ -561,7 +558,7 @@ export function createIpsecRoutes(dependencies = ipsecDefaults): FastifyPluginAs const ike2Peers = selectIke2Peers(state.peers) const caName = resolveIke2CaName(state.certs, resolveIke2ServerCert(state.certs, ike2Peers, state.identities)) const serverCertName = String(resolveIke2ServerCert(state.certs, ike2Peers, state.identities)?.name ?? "").trim() - const certName = remoteCert || (managed && userName ? clientCertName(userName) : "") + const certName = remoteCert if (certName && certName !== caName && certName !== serverCertName) { await client.removeCertificate(certName) } diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 3eda8b45..e8a48400 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-10: завершён12.5.115 — UI удаления identity позволяет сохранить сертификат; при общих refs он unchecked/disabled и результат объяснён. Source TypeScript/ESLint/production frontend build, E2E26 unique accepted (22+4 corrected), live dialogs4/4 и один mobile DELETE с removeCertificate=false. Native cert ID/другая identity сохранены, original online восстановлен, cleanup13×2 passed. Backend ux-refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Следующий116 — исключить удаление сертификата, угаданного по имени клиента; затем итоговая регрессия.12.5 открыт. +Актуальное продолжение 2026-10-10: завершён12.5.116 — DELETE клиента больше не угадывает certificate по display name; удаляется только прямо привязанный remote-certificate либо явно выбранный cert. PSK UI объясняет сохранение сертификатов. Backend114 state/8 scripts, build/TypeScript/ESLint, E2E28/28, live dialogs4/4/mobile PSK DELETE и native сохранение unbound cert ID passed; before bug доказан на собственном decoy. Backend/frontend ux-bound116/version1.0.0-test.bound116, cleanup13×2. Следующий117 — общая UI-регрессия (основной набор1244 запущен), контекст и сверка текущего source/runtime;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). @@ -1171,3 +1171,21 @@ Frontend ux-keepcert115/version1.0.0-test.keepcert115 развёрнут; backen | Референс | Схема | | --- | --- | | [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → прямой объём удаления → сохранить общий сертификат → подтверждённый API результат | + +## 12.5.116 — IPsec: не удалять сертификат, угаданный по имени PSK (2026-10-10) + +Устранена подтверждённая ошибка default DELETE: при пустом remote-certificate managed display name преобразовывалось в ipsec-user-, и несвязанный сертификат мог удаляться вместе с PSK identity. Теперь имя клиента не является доказательством связи: certificate deletion/preflight используют только явный remote-certificate либо выбранный cert target. Прямая одиночная certificate привязка по-прежнему допускает удаление; shared refs блокируются до записи как114. PSK диалог объясняет сохранение сертификатов; описания mode-config уточняют, что удаляются только неиспользуемые объекты приложения. + +Изменены routes/ipsec.ts, mutation test, IPsec page и delete E2E. Все8 backend scripts, state114, backend build/source TypeScript/scoped ESLint passed. HTTP тест подтвердил PSK name collision: один identity DELETE, removeCertificate0; positive explicit binding: identity+certificate writes2. E2E28 unique passed39.6s, включая PSK описание/сохранение отдельной строки1280/320 и профильную регрессию. Vendor/ReUI/base-nova не менялись. + +Before на backend114: создана собственная PSK identity с managed name, совпадающим со slug собственного unused signed decoy certificate. DELETE200 удалил оба объекта, хотя remote-certificate пуст. After на bound116: четыре live PSK диалога (dark/light1280/320), один DELETE через light320 UI, PSK удалён, тот же native ID/имя несвязанного сертификата сохранены. Browser не использовал mocks; APIwrites1, pageerrors0. Исходная certificate IKE сессия/online/198.18.116.250 сохранялись. Это не тест новой PSK сессии/throughput. Просмотрен light320 screenshot. + +Обе части собраны последовательно и развёрнуты как ux-bound116/version1.0.0-test.bound116. Base d52c48e/context backend build-refs114, frontend build-keepcert115-front; по одному override для route/page, два source bytehash и compiled backend route bytehash совпали. Frontend compiled reproducibility не заявлена, label bound116 неGitSHA. Private rollback pre-bound116.env и прежние образы оставлены. Backend/PG healthy; frontend browser/HTTP подтверждён, healthcheck отсутствует. На VM25GB после сборок3.6GB свободно/86% занято; ресурсов достаточно для текущих проверок, production performance не оценивалась. + +Оба native запуска очистили собственные fixtures, baseline13 коллекций×2 совпал; сохранены logging/default/public/metadata/195.195.195.195/32. Пароли прежние, оба CHR/SSH доступны. API DELETE вызывает history recording с dedupe; число новых revisions отдельно не заявляется. Протоколы: [binding](ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json), [live UI](ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json). + +Следующий117 уже запущен: единая основная UI-регрессия1244 сценария на текущем frontend source, затем контекстный набор и сверка source/build/runtime. Внешние аккаунты/Portal/Telegram/client OS/scale/multi-WAN и одновременные native IKE sessions остаются в матрице. + +| Референс | Схема | +| --- | --- | +| [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → явная certificate связь → preflight → удаление выбранных объектов / сохранение несвязанных | diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 2d8bb29a..4a87c0f9 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-10: завершён12.5.115 — UI удаления identity позволяет сохранить сертификат; при общих refs он unchecked/disabled и результат объяснён. Source TypeScript/ESLint/production frontend build, E2E26 unique accepted (22+4 corrected), live dialogs4/4 и один mobile DELETE с removeCertificate=false. Native cert ID/другая identity сохранены, original online восстановлен, cleanup13×2 passed. Backend ux-refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Следующий116 — исключить удаление сертификата, угаданного по имени клиента; затем итоговая регрессия.12.5 открыт. +Актуальное продолжение 2026-10-10: завершён12.5.116 — DELETE клиента больше не угадывает certificate по display name; удаляется только прямо привязанный remote-certificate либо явно выбранный cert. PSK UI объясняет сохранение сертификатов. Backend114 state/8 scripts, build/TypeScript/ESLint, E2E28/28, live dialogs4/4/mobile PSK DELETE и native сохранение unbound cert ID passed; before bug доказан на собственном decoy. Backend/frontend ux-bound116/version1.0.0-test.bound116, cleanup13×2. Следующий117 — общая UI-регрессия (основной набор1244 запущен), контекст и сверка текущего source/runtime;12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-10. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-10 -Актуальное продолжение 2026-10-10: завершён12.5.115 — UI удаления identity позволяет сохранить сертификат; при общих refs он unchecked/disabled и результат объяснён. Source TypeScript/ESLint/production frontend build, E2E26 unique accepted (22+4 corrected), live dialogs4/4 и один mobile DELETE с removeCertificate=false. Native cert ID/другая identity сохранены, original online восстановлен, cleanup13×2 passed. Backend ux-refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Следующий116 — исключить удаление сертификата, угаданного по имени клиента; затем итоговая регрессия.12.5 открыт. +Актуальное продолжение 2026-10-10: завершён12.5.116 — DELETE клиента больше не угадывает certificate по display name; удаляется только прямо привязанный remote-certificate либо явно выбранный cert. PSK UI объясняет сохранение сертификатов. Backend114 state/8 scripts, build/TypeScript/ESLint, E2E28/28, live dialogs4/4/mobile PSK DELETE и native сохранение unbound cert ID passed; before bug доказан на собственном decoy. Backend/frontend ux-bound116/version1.0.0-test.bound116, cleanup13×2. Следующий117 — общая UI-регрессия (основной набор1244 запущен), контекст и сверка текущего source/runtime;12.5 открыт. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 112ccfd9..b74bf8e2 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-10: завершён12.5.115 — UI удаления identity позволяет сохранить сертификат; при общих refs он unchecked/disabled и результат объяснён. Source TypeScript/ESLint/production frontend build, E2E26 unique accepted (22+4 corrected), live dialogs4/4 и один mobile DELETE с removeCertificate=false. Native cert ID/другая identity сохранены, original online восстановлен, cleanup13×2 passed. Backend ux-refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Следующий116 — исключить удаление сертификата, угаданного по имени клиента; затем итоговая регрессия.12.5 открыт. +Актуальное продолжение 2026-10-10: завершён12.5.116 — DELETE клиента больше не угадывает certificate по display name; удаляется только прямо привязанный remote-certificate либо явно выбранный cert. PSK UI объясняет сохранение сертификатов. Backend114 state/8 scripts, build/TypeScript/ESLint, E2E28/28, live dialogs4/4/mobile PSK DELETE и native сохранение unbound cert ID passed; before bug доказан на собственном decoy. Backend/frontend ux-bound116/version1.0.0-test.bound116, cleanup13×2. Следующий117 — общая UI-регрессия (основной набор1244 запущен), контекст и сверка текущего source/runtime;12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -36,7 +36,25 @@ | 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–115 и ограничения ниже. Runtime backend ux-refs114/frontend ux-keepcert115. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–116 и ограничения ниже. Runtime backend/frontend ux-bound116. | + +## 12.5.116 — IPsec: не удалять сертификат, угаданный по имени PSK (2026-10-10) + +Устранена подтверждённая ошибка default DELETE: при пустом remote-certificate managed display name преобразовывалось в ipsec-user-, и несвязанный сертификат мог удаляться вместе с PSK identity. Теперь имя клиента не является доказательством связи: certificate deletion/preflight используют только явный remote-certificate либо выбранный cert target. Прямая одиночная certificate привязка по-прежнему допускает удаление; shared refs блокируются до записи как114. PSK диалог объясняет сохранение сертификатов; описания mode-config уточняют, что удаляются только неиспользуемые объекты приложения. + +Изменены routes/ipsec.ts, mutation test, IPsec page и delete E2E. Все8 backend scripts, state114, backend build/source TypeScript/scoped ESLint passed. HTTP тест подтвердил PSK name collision: один identity DELETE, removeCertificate0; positive explicit binding: identity+certificate writes2. E2E28 unique passed39.6s, включая PSK описание/сохранение отдельной строки1280/320 и профильную регрессию. Vendor/ReUI/base-nova не менялись. + +Before на backend114: создана собственная PSK identity с managed name, совпадающим со slug собственного unused signed decoy certificate. DELETE200 удалил оба объекта, хотя remote-certificate пуст. After на bound116: четыре live PSK диалога (dark/light1280/320), один DELETE через light320 UI, PSK удалён, тот же native ID/имя несвязанного сертификата сохранены. Browser не использовал mocks; APIwrites1, pageerrors0. Исходная certificate IKE сессия/online/198.18.116.250 сохранялись. Это не тест новой PSK сессии/throughput. Просмотрен light320 screenshot. + +Обе части собраны последовательно и развёрнуты как ux-bound116/version1.0.0-test.bound116. Base d52c48e/context backend build-refs114, frontend build-keepcert115-front; по одному override для route/page, два source bytehash и compiled backend route bytehash совпали. Frontend compiled reproducibility не заявлена, label bound116 неGitSHA. Private rollback pre-bound116.env и прежние образы оставлены. Backend/PG healthy; frontend browser/HTTP подтверждён, healthcheck отсутствует. На VM25GB после сборок3.6GB свободно/86% занято; ресурсов достаточно для текущих проверок, production performance не оценивалась. + +Оба native запуска очистили собственные fixtures, baseline13 коллекций×2 совпал; сохранены logging/default/public/metadata/195.195.195.195/32. Пароли прежние, оба CHR/SSH доступны. API DELETE вызывает history recording с dedupe; число новых revisions отдельно не заявляется. Протоколы: [binding](ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json), [live UI](ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json). + +Следующий117 уже запущен: единая основная UI-регрессия1244 сценария на текущем frontend source, затем контекстный набор и сверка source/build/runtime. Внешние аккаунты/Portal/Telegram/client OS/scale/multi-WAN и одновременные native IKE sessions остаются в матрице. + +| Референс | Схема | +| --- | --- | +| [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → явная certificate связь → preflight → удаление выбранных объектов / сохранение несвязанных | ## 12.5.115 — IPsec: удалить identity и сохранить общий сертификат через UI (2026-10-10) diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index d371b43c..4d99d8b6 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,8 +1,8 @@ # Оставшаяся внешняя приёмка — 2026-10-10 -Это актуальная матрица после 12.5.115. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.116. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-refs114`, frontend `ux-keepcert115`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend/frontend `ux-bound116`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -12,7 +12,7 @@ | GRE | Native lifecycle, metadata, live формы/меню/URL/Ping53–57; GRE используется в OSPF/BGP/IPsec последующих блоков; native IPv6 ULA inner109 | IPv6 outer и application IPv6 interface address lifecycle, независимые WAN, масштаб и длительное восстановление; принятие GRE не означает приёмку всех сценариев автоматического расписания | | WireGuard | Native handshake/data plane58, deletion60, live create/peer/disable/enable/delete62, mobile64, export/import67–74; native IPv6 ULA inner/allowedIps/handshake109 | Клиентские ОС и произвольные конфигурации, IPv6 outer/scale и application IPv6 interface address lifecycle, физический lost ACK и устойчивое восстановление; `.conf` с placeholders — шаблон, не принятый клиентский tunnel | | VXLAN | Собственная пара/private Ping/catalog/URL/export75–76; bridge/FDB/port loss/recovery107; IPv6 ULA inner по IPv4 outer108 | API приложения read-only: create/delete проверены native. IPv6 outer, масштаб/производительность, профильные mutation RBAC/concurrency отдельно не приняты | -| IPsec | PSK и сертификатное шифрование77–82, active peer/online/mode-config, scoped NAT/group, partial/unknown/applied receipts, restore conflicts, JWT; full DN/специальные символы/online110; literal CN namespace guards и explicit FQDN111; certificate/PSK shared ID unknown/recovery112; multiple sessions controlled snapshots113; shared certificate rows/target guards114; preserve-certificate UI/native DELETE115 | Реальные одновременные sessions с одним ID, Связь удаляемого certificate с identity без name-based guessing; native edit identical matching rules rejected; клиентские Windows/macOS/iOS/Android, прочие DN форматы/custom OIDs/IKE ID namespaces, полная PKI и policy/profile/proposal restore, физический disconnect, durable retry/multi-instance, IPv6/maximum throughput | +| IPsec | PSK и сертификатное шифрование77–82, active peer/online/mode-config, scoped NAT/group, partial/unknown/applied receipts, restore conflicts, JWT; full DN/специальные символы/online110; literal CN namespace guards и explicit FQDN111; certificate/PSK shared ID unknown/recovery112; multiple sessions controlled snapshots113; shared certificate rows/target guards114; preserve-certificate UI/native DELETE115; explicit deletion binding/PSK collision116 | Реальные одновременные sessions с одним ID, Итоговая source/runtime сверка и общая регрессия текущей версии; native edit identical matching rules rejected; клиентские Windows/macOS/iOS/Android, прочие DN форматы/custom OIDs/IKE ID namespaces, полная PKI и policy/profile/proposal restore, физический disconnect, durable retry/multi-instance, IPv6/maximum throughput | | OSPF/BFD | Real Full/own routes84–85, раздельные наблюдения BFD86, effective disabled/optimizer cost/partial/RBAC87, alternate path90 и CIDR91 | Sustained BFD-only gate не принят: в86 сосед мог вновь статьFull при доступныхHello. В90 переходный Ping18/40 с55% loss: бесшовность не принята. REST timing — наблюдаемая граница, не SLA; IPv6/scale/independent WAN открыты | | BGP | Established83, exact /32 exchange/API/UI/administrative withdrawal92, BFD primary GRE loss/alternate/recovery93 | Hold timer/TCP loss безBFD принят104; packet loss в переходе, разные внешние peers/WAN policy, IPv6/scale. Два логических пути используют тот же public WAN; physical redundancy не принята | | Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята | @@ -25,7 +25,7 @@ ## Следующая последовательность 1. **12.5.95 завершён:** [source/context/compiled audit](ux-ui/acceptance/deployment-source-audit-2026-10-10.json), production source drift не выявлен; нормализация и исключения явно указаны. -2. **12.5.96 timezone завершён:** [6 live UI cases](ux-ui/acceptance/browser-speed-timezone-2026-10-10.json). 97 HTTP receipt disconnect принят: [протокол](ux-ui/acceptance/speed-disconnect-2026-10-10.json). 98 сообщение неизвестного native результата исправлено/развёрнуто. 99 main/context завершён с точной записью одного failure/delta;100 transport и101 accessibility исправлены/развёрнуты.102 malformed PATCH исправлен/принят.103 packet rows/public/GRE приняты в границах коротких samples; initial loss/root cause остаётся открытым.104 hold/TCP loss/recovery принят.105 source/runtime/evidence сверка passed.106 read-only readiness: auth disabled/Portal localhost/bot and chat unconfigured.107 — native bridge/FDB/loss/recovery принят.108 — IPv6 inner VXLAN принят, initial0/3 и1/3 остаются в протоколе;109 — native GRE/WireGuard IPv6 inner принят; readers /ip/address не показывают native IPv6 interface addresses.110 — полный native DN/online исправлен и принят;111 — literal CN guards/real explicit FQDN accepted;112 — certificate/PSK alias ambiguity marked unknown/recovery accepted;113 — repeated native ID groups исправлены на controlled snapshots; 114 — multiple refs/target guards/native conflict приняты в описанных границах. 115 — UI сохранения сертификата/native DELETE принят; следующий116 — запрет name-based guessing сертификата. Реальные интеграции требуют тестовых доступов и отдельного разрешения на сообщение. +2. **12.5.96 timezone завершён:** [6 live UI cases](ux-ui/acceptance/browser-speed-timezone-2026-10-10.json). 97 HTTP receipt disconnect принят: [протокол](ux-ui/acceptance/speed-disconnect-2026-10-10.json). 98 сообщение неизвестного native результата исправлено/развёрнуто. 99 main/context завершён с точной записью одного failure/delta;100 transport и101 accessibility исправлены/развёрнуты.102 malformed PATCH исправлен/принят.103 packet rows/public/GRE приняты в границах коротких samples; initial loss/root cause остаётся открытым.104 hold/TCP loss/recovery принят.105 source/runtime/evidence сверка passed.106 read-only readiness: auth disabled/Portal localhost/bot and chat unconfigured.107 — native bridge/FDB/loss/recovery принят.108 — IPv6 inner VXLAN принят, initial0/3 и1/3 остаются в протоколе;109 — native GRE/WireGuard IPv6 inner принят; readers /ip/address не показывают native IPv6 interface addresses.110 — полный native DN/online исправлен и принят;111 — literal CN guards/real explicit FQDN accepted;112 — certificate/PSK alias ambiguity marked unknown/recovery accepted;113 — repeated native ID groups исправлены на controlled snapshots; 114 — multiple refs/target guards/native conflict приняты в описанных границах. 115 — UI сохранения сертификата/native DELETE принят; 116 — name-based guessing удалён/live PSK collision принят;117 — общая регрессия запущена. Реальные интеграции требуют тестовых доступов и отдельного разрешения на сообщение. 3. **Заключительный профильный контроль:** после последнего исправления собрать только затронутую часть, проверить её live и обновить матрицу. Общую регрессию выполнить один раз на согласованном runtime. 4. **Внешние зависимости:** Telegram/Portal/client OS/multi-WAN/scale проходят отдельную приёмку при наличии нужных аккаунтов, устройств и разрешений. Лабораторный результат двух CHR не закрывает их автоматически. @@ -86,3 +86,21 @@ Frontend ux-keepcert115/version1.0.0-test.keepcert115 развёрнут; backen | Референс | Схема | | --- | --- | | [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → прямой объём удаления → сохранить общий сертификат → подтверждённый API результат | + +## 12.5.116 — IPsec: не удалять сертификат, угаданный по имени PSK (2026-10-10) + +Устранена подтверждённая ошибка default DELETE: при пустом remote-certificate managed display name преобразовывалось в ipsec-user-, и несвязанный сертификат мог удаляться вместе с PSK identity. Теперь имя клиента не является доказательством связи: certificate deletion/preflight используют только явный remote-certificate либо выбранный cert target. Прямая одиночная certificate привязка по-прежнему допускает удаление; shared refs блокируются до записи как114. PSK диалог объясняет сохранение сертификатов; описания mode-config уточняют, что удаляются только неиспользуемые объекты приложения. + +Изменены routes/ipsec.ts, mutation test, IPsec page и delete E2E. Все8 backend scripts, state114, backend build/source TypeScript/scoped ESLint passed. HTTP тест подтвердил PSK name collision: один identity DELETE, removeCertificate0; positive explicit binding: identity+certificate writes2. E2E28 unique passed39.6s, включая PSK описание/сохранение отдельной строки1280/320 и профильную регрессию. Vendor/ReUI/base-nova не менялись. + +Before на backend114: создана собственная PSK identity с managed name, совпадающим со slug собственного unused signed decoy certificate. DELETE200 удалил оба объекта, хотя remote-certificate пуст. After на bound116: четыре live PSK диалога (dark/light1280/320), один DELETE через light320 UI, PSK удалён, тот же native ID/имя несвязанного сертификата сохранены. Browser не использовал mocks; APIwrites1, pageerrors0. Исходная certificate IKE сессия/online/198.18.116.250 сохранялись. Это не тест новой PSK сессии/throughput. Просмотрен light320 screenshot. + +Обе части собраны последовательно и развёрнуты как ux-bound116/version1.0.0-test.bound116. Base d52c48e/context backend build-refs114, frontend build-keepcert115-front; по одному override для route/page, два source bytehash и compiled backend route bytehash совпали. Frontend compiled reproducibility не заявлена, label bound116 неGitSHA. Private rollback pre-bound116.env и прежние образы оставлены. Backend/PG healthy; frontend browser/HTTP подтверждён, healthcheck отсутствует. На VM25GB после сборок3.6GB свободно/86% занято; ресурсов достаточно для текущих проверок, production performance не оценивалась. + +Оба native запуска очистили собственные fixtures, baseline13 коллекций×2 совпал; сохранены logging/default/public/metadata/195.195.195.195/32. Пароли прежние, оба CHR/SSH доступны. API DELETE вызывает history recording с dedupe; число новых revisions отдельно не заявляется. Протоколы: [binding](ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json), [live UI](ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json). + +Следующий117 уже запущен: единая основная UI-регрессия1244 сценария на текущем frontend source, затем контекстный набор и сверка source/build/runtime. Внешние аккаунты/Portal/Telegram/client OS/scale/multi-WAN и одновременные native IKE sessions остаются в матрице. + +| Референс | Схема | +| --- | --- | +| [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → явная certificate связь → preflight → удаление выбранных объектов / сохранение несвязанных | diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index f2787786..1d1999b3 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-10: завершён12.5.115 — UI удаления identity позволяет сохранить сертификат; при общих refs он unchecked/disabled и результат объяснён. Source TypeScript/ESLint/production frontend build, E2E26 unique accepted (22+4 corrected), live dialogs4/4 и один mobile DELETE с removeCertificate=false. Native cert ID/другая identity сохранены, original online восстановлен, cleanup13×2 passed. Backend ux-refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Следующий116 — исключить удаление сертификата, угаданного по имени клиента; затем итоговая регрессия.12.5 открыт. +Актуальное продолжение 2026-10-10: завершён12.5.116 — DELETE клиента больше не угадывает certificate по display name; удаляется только прямо привязанный remote-certificate либо явно выбранный cert. PSK UI объясняет сохранение сертификатов. Backend114 state/8 scripts, build/TypeScript/ESLint, E2E28/28, live dialogs4/4/mobile PSK DELETE и native сохранение unbound cert ID passed; before bug доказан на собственном decoy. Backend/frontend ux-bound116/version1.0.0-test.bound116, cleanup13×2. Следующий117 — общая UI-регрессия (основной набор1244 запущен), контекст и сверка текущего source/runtime;12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -2476,3 +2476,21 @@ Frontend ux-keepcert115/version1.0.0-test.keepcert115 развёрнут; backen | Референс | Схема | | --- | --- | | [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → прямой объём удаления → сохранить общий сертификат → подтверждённый API результат | + +## 12.5.116 — IPsec: не удалять сертификат, угаданный по имени PSK (2026-10-10) + +Устранена подтверждённая ошибка default DELETE: при пустом remote-certificate managed display name преобразовывалось в ipsec-user-, и несвязанный сертификат мог удаляться вместе с PSK identity. Теперь имя клиента не является доказательством связи: certificate deletion/preflight используют только явный remote-certificate либо выбранный cert target. Прямая одиночная certificate привязка по-прежнему допускает удаление; shared refs блокируются до записи как114. PSK диалог объясняет сохранение сертификатов; описания mode-config уточняют, что удаляются только неиспользуемые объекты приложения. + +Изменены routes/ipsec.ts, mutation test, IPsec page и delete E2E. Все8 backend scripts, state114, backend build/source TypeScript/scoped ESLint passed. HTTP тест подтвердил PSK name collision: один identity DELETE, removeCertificate0; positive explicit binding: identity+certificate writes2. E2E28 unique passed39.6s, включая PSK описание/сохранение отдельной строки1280/320 и профильную регрессию. Vendor/ReUI/base-nova не менялись. + +Before на backend114: создана собственная PSK identity с managed name, совпадающим со slug собственного unused signed decoy certificate. DELETE200 удалил оба объекта, хотя remote-certificate пуст. After на bound116: четыре live PSK диалога (dark/light1280/320), один DELETE через light320 UI, PSK удалён, тот же native ID/имя несвязанного сертификата сохранены. Browser не использовал mocks; APIwrites1, pageerrors0. Исходная certificate IKE сессия/online/198.18.116.250 сохранялись. Это не тест новой PSK сессии/throughput. Просмотрен light320 screenshot. + +Обе части собраны последовательно и развёрнуты как ux-bound116/version1.0.0-test.bound116. Base d52c48e/context backend build-refs114, frontend build-keepcert115-front; по одному override для route/page, два source bytehash и compiled backend route bytehash совпали. Frontend compiled reproducibility не заявлена, label bound116 неGitSHA. Private rollback pre-bound116.env и прежние образы оставлены. Backend/PG healthy; frontend browser/HTTP подтверждён, healthcheck отсутствует. На VM25GB после сборок3.6GB свободно/86% занято; ресурсов достаточно для текущих проверок, production performance не оценивалась. + +Оба native запуска очистили собственные fixtures, baseline13 коллекций×2 совпал; сохранены logging/default/public/metadata/195.195.195.195/32. Пароли прежние, оба CHR/SSH доступны. API DELETE вызывает history recording с dedupe; число новых revisions отдельно не заявляется. Протоколы: [binding](ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json), [live UI](ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json). + +Следующий117 уже запущен: единая основная UI-регрессия1244 сценария на текущем frontend source, затем контекстный набор и сверка source/build/runtime. Внешние аккаунты/Portal/Telegram/client OS/scale/multi-WAN и одновременные native IKE sessions остаются в матрице. + +| Референс | Схема | +| --- | --- | +| [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → явная certificate связь → preflight → удаление выбранных объектов / сохранение несвязанных | diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index a7798ff3..4023e0ff 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Сервер161.104.46.222: backend ux-refs114/version1.0.0-test.refs114, frontend ux-keepcert115/version1.0.0-test.keepcert115. Backend/PG healthy, четыре live подтверждения и один mobile DELETE identity с сохранением общего certificate ID passed; cleanup13×2. Оба CHR доступны, пароли прежние. Следующий116 — исключить name-based удаление несвязанного сертификата. +Сервер161.104.46.222: backend/frontend ux-bound116/version1.0.0-test.bound116. Backend/PG healthy, четыре live PSK подтверждения/один mobile DELETE, unbound certificate native ID сохранён, исходная IKE сессия стабильна. Before name-collision bug доказан на своём decoy. Cleanup13×2, оба CHR доступны/пароли прежние. Следующий117 — общая UI-регрессия и source/runtime сверка. | Параметр | Проверенное состояние | | --- | --- | @@ -14,7 +14,7 @@ | PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed | | Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data | | Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска | -| Образы приложения | mmapp-test/backend:ux-refs114 и mmapp-test/frontend:ux-keepcert115; запущены | +| Образы приложения | mmapp-test/backend:ux-bound116 и mmapp-test/frontend:ux-bound116; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -902,3 +902,21 @@ Frontend ux-keepcert115/version1.0.0-test.keepcert115 развёрнут; backen | Референс | Схема | | --- | --- | | [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → прямой объём удаления → сохранить общий сертификат → подтверждённый API результат | + +## 12.5.116 — IPsec: не удалять сертификат, угаданный по имени PSK (2026-10-10) + +Устранена подтверждённая ошибка default DELETE: при пустом remote-certificate managed display name преобразовывалось в ipsec-user-, и несвязанный сертификат мог удаляться вместе с PSK identity. Теперь имя клиента не является доказательством связи: certificate deletion/preflight используют только явный remote-certificate либо выбранный cert target. Прямая одиночная certificate привязка по-прежнему допускает удаление; shared refs блокируются до записи как114. PSK диалог объясняет сохранение сертификатов; описания mode-config уточняют, что удаляются только неиспользуемые объекты приложения. + +Изменены routes/ipsec.ts, mutation test, IPsec page и delete E2E. Все8 backend scripts, state114, backend build/source TypeScript/scoped ESLint passed. HTTP тест подтвердил PSK name collision: один identity DELETE, removeCertificate0; positive explicit binding: identity+certificate writes2. E2E28 unique passed39.6s, включая PSK описание/сохранение отдельной строки1280/320 и профильную регрессию. Vendor/ReUI/base-nova не менялись. + +Before на backend114: создана собственная PSK identity с managed name, совпадающим со slug собственного unused signed decoy certificate. DELETE200 удалил оба объекта, хотя remote-certificate пуст. After на bound116: четыре live PSK диалога (dark/light1280/320), один DELETE через light320 UI, PSK удалён, тот же native ID/имя несвязанного сертификата сохранены. Browser не использовал mocks; APIwrites1, pageerrors0. Исходная certificate IKE сессия/online/198.18.116.250 сохранялись. Это не тест новой PSK сессии/throughput. Просмотрен light320 screenshot. + +Обе части собраны последовательно и развёрнуты как ux-bound116/version1.0.0-test.bound116. Base d52c48e/context backend build-refs114, frontend build-keepcert115-front; по одному override для route/page, два source bytehash и compiled backend route bytehash совпали. Frontend compiled reproducibility не заявлена, label bound116 неGitSHA. Private rollback pre-bound116.env и прежние образы оставлены. Backend/PG healthy; frontend browser/HTTP подтверждён, healthcheck отсутствует. На VM25GB после сборок3.6GB свободно/86% занято; ресурсов достаточно для текущих проверок, production performance не оценивалась. + +Оба native запуска очистили собственные fixtures, baseline13 коллекций×2 совпал; сохранены logging/default/public/metadata/195.195.195.195/32. Пароли прежние, оба CHR/SSH доступны. API DELETE вызывает history recording с dedupe; число новых revisions отдельно не заявляется. Протоколы: [binding](ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json), [live UI](ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json). + +Следующий117 уже запущен: единая основная UI-регрессия1244 сценария на текущем frontend source, затем контекстный набор и сверка source/build/runtime. Внешние аккаунты/Portal/Telegram/client OS/scale/multi-WAN и одновременные native IKE sessions остаются в матрице. + +| Референс | Схема | +| --- | --- | +| [ReUI Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Выбранная identity → явная certificate связь → preflight → удаление выбранных объектов / сохранение несвязанных | diff --git a/docs/ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json b/docs/ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json new file mode 100644 index 00000000..87c6617c --- /dev/null +++ b/docs/ux-ui/acceptance/browser-ipsec-bound-2026-10-10.json @@ -0,0 +1,51 @@ +{ + "date": "2026-10-10", + "runtime": { + "backend": "ux-bound116", + "frontend": "ux-bound116" + }, + "httpMocks": false, + "cases": [ + { + "theme": "dark", + "width": 1280, + "pskCertificatePreservationExplained": true, + "scopeExplained": true, + "noDialogOverflow": true, + "confirmedDelete": false + }, + { + "theme": "dark", + "width": 320, + "pskCertificatePreservationExplained": true, + "scopeExplained": true, + "noDialogOverflow": true, + "confirmedDelete": false + }, + { + "theme": "light", + "width": 1280, + "pskCertificatePreservationExplained": true, + "scopeExplained": true, + "noDialogOverflow": true, + "confirmedDelete": false + }, + { + "theme": "light", + "width": 320, + "pskCertificatePreservationExplained": true, + "scopeExplained": true, + "noDialogOverflow": true, + "confirmedDelete": true + } + ], + "pageErrors": [], + "applicationWrites": [ + { + "method": "DELETE", + "path": "/api/ipsec/users/1/identity%3A*B", + "query": "" + } + ], + "scope": "Four real PSK dialogs and one UI DELETE preserving unbound same-named certificate" +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json b/docs/ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json new file mode 100644 index 00000000..331e9297 --- /dev/null +++ b/docs/ux-ui/acceptance/ipsec-certificate-binding-2026-10-10.json @@ -0,0 +1,449 @@ +{ + "date": "2026-10-10", + "block": "12.5.116", + "deployment": { + "date": "2026-10-10", + "baseCommit": "d52c48e", + "backendBaseContext": "build-refs114", + "frontendBaseContext": "build-keepcert115-front", + "backendContext": "build-bound116", + "frontendContext": "build-bound116-front", + "sourceHashes": { + "backend/src/routes/ipsec.ts": "83eef38ef796b08f9b9e2d5827fcbbdcb8799bad2f18ba52aeb6a33d94226290", + "app/(main)/ipsec/page.tsx": "2d454336d6f0090c754e2f61c68a8d67c95b9fa5ec807996230861e0beb60092" + }, + "compiledHashes": { + "backend/dist/routes/ipsec.js": "3318fde98abc367bda3cd1d444c82d8c813b32aa05144a59ccca00f7887d1f53" + }, + "containers": { + "backend": { + "imageTag": "mmapp-test/backend:ux-bound116", + "imageId": "sha256:98a8f1c55c4dffc0eddd5b75a2ac7e420ec612d13b269e2631654409cfcf9f98", + "state": "running", + "health": "healthy" + }, + "frontend": { + "imageTag": "mmapp-test/frontend:ux-bound116", + "imageId": "sha256:d641b518fa35291e2c25dcabdef13b730c307a424d3b96bc9847df79022ead28", + "state": "running", + "health": null + }, + "postgres": { + "imageTag": "postgres:18-alpine", + "imageId": "sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873", + "state": "running", + "health": "healthy" + } + } + }, + "sourceByteHashesMatch": true, + "compiledBackendRouteHashMatches": true, + "before": { + "date": "2026-10-10", + "block": "12.5.116", + "name": "mm-ipsec116-1791641648556", + "cn": "mm-ipsec116-1791641648556", + "observations": [ + { + "serverId": 1, + "active": [ + { + ".id": "*1", + "id": "CN=mm-ipsec116-1791641648556", + "state": "established", + "dynamic-address": "198.18.116.250", + "remote-address": "104.165.244.7", + "uptime": "1s" + } + ], + "catalog": [ + { + "name": "CN=mm-ipsec116-1791641648556", + "commonName": "CN=mm-ipsec116-1791641648556", + "certName": "ipsec-user-mm-ipsec116-1791641648556-decoy", + "online": false + }, + { + "name": "mm-ipsec116-1791641648556", + "commonName": "mm-ipsec116-1791641648556", + "certName": "ipsec-user-mm-ipsec116-1791641648556", + "online": true, + "activeAddress": "198.18.116.250" + } + ] + }, + { + "serverId": 2, + "active": [ + { + ".id": "*1", + "id": "194.226.121.150", + "state": "established", + "remote-address": "194.226.121.150", + "uptime": "0s" + } + ], + "catalog": [] + } + ], + "cleanup": [ + { + "serverId": 1, + "/system/logging": true, + "/ip/ipsec/identity": true, + "/ip/ipsec/peer": true, + "/ip/ipsec/policy": true, + "/ip/ipsec/policy/group": true, + "/ip/ipsec/mode-config": true, + "/ip/ipsec/proposal": true, + "/ip/ipsec/profile": true, + "/certificate": true, + "/file": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/nat": true + }, + { + "serverId": 2, + "/system/logging": true, + "/ip/ipsec/identity": true, + "/ip/ipsec/peer": true, + "/ip/ipsec/policy": true, + "/ip/ipsec/policy/group": true, + "/ip/ipsec/mode-config": true, + "/ip/ipsec/proposal": true, + "/ip/ipsec/profile": true, + "/certificate": true, + "/file": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/nat": true + } + ], + "databaseWrites": "One successful API DELETE invokes history recording with deduplication", + "certificate": { + "name": "ipsec-user-mm-ipsec116-1791641648556", + "common-name": "mm-ipsec116-1791641648556", + "ca": "mm-ipsec116-1791641648556-ca", + "key-usage": "digital-signature,key-encipherment,tls-client" + }, + "certificates": [ + [ + { + "name": "mm-ipsec116-1791641648556-ca", + "trusted": "true", + "trust-store": "all", + "invalid-before": "2026-10-10 17:14:10", + "invalid-after": "2026-10-12 17:14:10", + "common-name": "mm-ipsec116-1791641648556-CA" + }, + { + "name": "mm-ipsec116-1791641648556-server", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791641648556-ca", + "invalid-before": "2026-10-10 17:14:11", + "invalid-after": "2026-10-12 17:14:11", + "common-name": "194.226.121.150" + }, + { + "name": "ipsec-user-mm-ipsec116-1791641648556", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791641648556-ca", + "invalid-before": "2026-10-10 17:14:11", + "invalid-after": "2026-10-12 17:14:11", + "common-name": "mm-ipsec116-1791641648556" + }, + { + "name": "ipsec-user-mm-ipsec116-1791641648556-decoy", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791641648556-ca", + "invalid-before": "2026-10-10 17:14:13", + "invalid-after": "2026-10-12 17:14:13", + "common-name": "CN=mm-ipsec116-1791641648556" + } + ], + [ + { + "name": "mm-ipsec116-1791641648556-client-imported", + "trusted": "true", + "trust-store": "ipsec", + "invalid-before": "2026-10-10 16:14:11", + "invalid-after": "2026-10-12 16:14:11", + "common-name": "mm-ipsec116-1791641648556" + }, + { + "name": "mm-ipsec116-1791641648556-ca-imported", + "trusted": "true", + "trust-store": "ipsec", + "invalid-before": "2026-10-10 16:14:10", + "invalid-after": "2026-10-12 16:14:10", + "common-name": "mm-ipsec116-1791641648556-CA" + } + ] + ], + "catalogCorrect": true, + "dnPhase": { + "active": [ + { + ".id": "*1", + "id": "CN=mm-ipsec116-1791641648556", + "state": "established", + "dynamic-address": "198.18.116.250", + "remote-address": "104.165.244.7", + "uptime": "1s" + } + ], + "clients": [ + { + "name": "CN=mm-ipsec116-1791641648556", + "commonName": "CN=mm-ipsec116-1791641648556", + "certName": "ipsec-user-mm-ipsec116-1791641648556-decoy", + "online": false + }, + { + "name": "mm-ipsec116-1791641648556", + "commonName": "mm-ipsec116-1791641648556", + "certName": "ipsec-user-mm-ipsec116-1791641648556", + "online": true, + "activeAddress": "198.18.116.250" + } + ], + "decoyOnline": false + }, + "fqdnPhase": { + "configuredRemoteId": "fqdn:mm-ipsec116-1791641648556.example", + "nativeId": "mm-ipsec116-1791641648556.example", + "assignedAddress": "198.18.116.250", + "originalOnline": true, + "decoyOnline": false + }, + "deletionReply": { + "ok": true + }, + "nameCollision": { + "pskUserName": "mm-ipsec116-1791641648556-decoy", + "unboundCertificateName": "ipsec-user-mm-ipsec116-1791641648556-decoy", + "unboundCertificateNativeId": "*9D", + "pskRemoved": true, + "certificatePreserved": false + }, + "originalSessionUnchanged": true + }, + "native": { + "date": "2026-10-10", + "block": "12.5.116", + "name": "mm-ipsec116-1791642066101", + "cn": "mm-ipsec116-1791642066101", + "observations": [ + { + "serverId": 1, + "active": [ + { + ".id": "*1", + "id": "CN=mm-ipsec116-1791642066101", + "state": "established", + "dynamic-address": "198.18.116.250", + "remote-address": "104.165.244.7", + "uptime": "1s" + } + ], + "catalog": [ + { + "name": "CN=mm-ipsec116-1791642066101", + "commonName": "CN=mm-ipsec116-1791642066101", + "certName": "ipsec-user-mm-ipsec116-1791642066101-decoy", + "online": false + }, + { + "name": "mm-ipsec116-1791642066101", + "commonName": "mm-ipsec116-1791642066101", + "certName": "ipsec-user-mm-ipsec116-1791642066101", + "online": true, + "activeAddress": "198.18.116.250" + } + ] + }, + { + "serverId": 2, + "active": [ + { + ".id": "*1", + "id": "194.226.121.150", + "state": "established", + "remote-address": "194.226.121.150", + "uptime": "1s" + } + ], + "catalog": [] + } + ], + "cleanup": [ + { + "serverId": 1, + "/system/logging": true, + "/ip/ipsec/identity": true, + "/ip/ipsec/peer": true, + "/ip/ipsec/policy": true, + "/ip/ipsec/policy/group": true, + "/ip/ipsec/mode-config": true, + "/ip/ipsec/proposal": true, + "/ip/ipsec/profile": true, + "/certificate": true, + "/file": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/nat": true + }, + { + "serverId": 2, + "/system/logging": true, + "/ip/ipsec/identity": true, + "/ip/ipsec/peer": true, + "/ip/ipsec/policy": true, + "/ip/ipsec/policy/group": true, + "/ip/ipsec/mode-config": true, + "/ip/ipsec/proposal": true, + "/ip/ipsec/profile": true, + "/certificate": true, + "/file": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/nat": true + } + ], + "databaseWrites": "One successful API DELETE invokes history recording with deduplication", + "certificate": { + "name": "ipsec-user-mm-ipsec116-1791642066101", + "common-name": "mm-ipsec116-1791642066101", + "ca": "mm-ipsec116-1791642066101-ca", + "key-usage": "digital-signature,key-encipherment,tls-client" + }, + "certificates": [ + [ + { + "name": "mm-ipsec116-1791642066101-ca", + "trusted": "true", + "trust-store": "all", + "invalid-before": "2026-10-10 17:21:07", + "invalid-after": "2026-10-12 17:21:07", + "common-name": "mm-ipsec116-1791642066101-CA" + }, + { + "name": "mm-ipsec116-1791642066101-server", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791642066101-ca", + "invalid-before": "2026-10-10 17:21:08", + "invalid-after": "2026-10-12 17:21:08", + "common-name": "194.226.121.150" + }, + { + "name": "ipsec-user-mm-ipsec116-1791642066101", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791642066101-ca", + "invalid-before": "2026-10-10 17:21:09", + "invalid-after": "2026-10-12 17:21:09", + "common-name": "mm-ipsec116-1791642066101" + }, + { + "name": "ipsec-user-mm-ipsec116-1791642066101-decoy", + "trusted": "false", + "trust-store": "all", + "ca": "mm-ipsec116-1791642066101-ca", + "invalid-before": "2026-10-10 17:21:09", + "invalid-after": "2026-10-12 17:21:09", + "common-name": "CN=mm-ipsec116-1791642066101" + } + ], + [ + { + "name": "mm-ipsec116-1791642066101-client-imported", + "trusted": "true", + "trust-store": "ipsec", + "invalid-before": "2026-10-10 16:21:09", + "invalid-after": "2026-10-12 16:21:09", + "common-name": "mm-ipsec116-1791642066101" + }, + { + "name": "mm-ipsec116-1791642066101-ca-imported", + "trusted": "true", + "trust-store": "ipsec", + "invalid-before": "2026-10-10 16:21:07", + "invalid-after": "2026-10-12 16:21:07", + "common-name": "mm-ipsec116-1791642066101-CA" + } + ] + ], + "catalogCorrect": true, + "dnPhase": { + "active": [ + { + ".id": "*1", + "id": "CN=mm-ipsec116-1791642066101", + "state": "established", + "dynamic-address": "198.18.116.250", + "remote-address": "104.165.244.7", + "uptime": "1s" + } + ], + "clients": [ + { + "name": "CN=mm-ipsec116-1791642066101", + "commonName": "CN=mm-ipsec116-1791642066101", + "certName": "ipsec-user-mm-ipsec116-1791642066101-decoy", + "online": false + }, + { + "name": "mm-ipsec116-1791642066101", + "commonName": "mm-ipsec116-1791642066101", + "certName": "ipsec-user-mm-ipsec116-1791642066101", + "online": true, + "activeAddress": "198.18.116.250" + } + ], + "decoyOnline": false + }, + "fqdnPhase": { + "configuredRemoteId": "fqdn:mm-ipsec116-1791642066101.example", + "nativeId": "mm-ipsec116-1791642066101.example", + "assignedAddress": "198.18.116.250", + "originalOnline": true, + "decoyOnline": false + }, + "pskUserName": "mm-ipsec116-1791642066101-decoy", + "deletionReply": { + "triggeredBy": "live UI" + }, + "nameCollision": { + "pskUserName": "mm-ipsec116-1791642066101-decoy", + "unboundCertificateName": "ipsec-user-mm-ipsec116-1791642066101-decoy", + "unboundCertificateNativeId": "*A1", + "pskRemoved": true, + "certificatePreserved": true + }, + "originalSessionUnchanged": true + }, + "browserEvidence": "browser-ipsec-bound-2026-10-10.json", + "local": { + "profileScripts": 8, + "stateChecks": 114, + "backendBuildPassed": true, + "frontendProductionBuildPassed": true, + "sourceTypeScriptPassed": true, + "eslintPassed": true, + "e2eUnique": 28, + "e2eSeconds": 39.6 + }, + "bounds": [ + "No certificate inference from managed user display name in DELETE; explicit remote-certificate/cert target only", + "Before reproduction intentionally removed only own unbound decoy certificate", + "After is one real mobile PSK UI DELETE; four confirmation dialogs", + "Signed certificate IKE connection remained established; no new PSK connection/data-plane test", + "History recording invoked with deduplication; inserted revision count not claimed", + "Full UI regression running separately in117" + ] +} diff --git a/docs/ux-ui/screens/ipsec-bound-dark-1280-2026-10-10.png b/docs/ux-ui/screens/ipsec-bound-dark-1280-2026-10-10.png new file mode 100644 index 00000000..bed369b2 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-bound-dark-1280-2026-10-10.png differ diff --git a/docs/ux-ui/screens/ipsec-bound-dark-320-2026-10-10.png b/docs/ux-ui/screens/ipsec-bound-dark-320-2026-10-10.png new file mode 100644 index 00000000..87d87c8d Binary files /dev/null and b/docs/ux-ui/screens/ipsec-bound-dark-320-2026-10-10.png differ diff --git a/docs/ux-ui/screens/ipsec-bound-light-1280-2026-10-10.png b/docs/ux-ui/screens/ipsec-bound-light-1280-2026-10-10.png new file mode 100644 index 00000000..8ef98810 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-bound-light-1280-2026-10-10.png differ diff --git a/docs/ux-ui/screens/ipsec-bound-light-320-2026-10-10.png b/docs/ux-ui/screens/ipsec-bound-light-320-2026-10-10.png new file mode 100644 index 00000000..b8d07ca4 Binary files /dev/null and b/docs/ux-ui/screens/ipsec-bound-light-320-2026-10-10.png differ diff --git a/tests/e2e/ipsec-delete.spec.ts b/tests/e2e/ipsec-delete.spec.ts index 82a0c252..a0788cdc 100644 --- a/tests/e2e/ipsec-delete.spec.ts +++ b/tests/e2e/ipsec-delete.spec.ts @@ -1,5 +1,29 @@ import { test, expect } from "@playwright/test" +for (const width of [1280,320]) { + test(`IPsec: удаление PSK объясняет сохранение сертификатов ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }) + let deleted = false + const certificate = { id: "cert", rosId: "cert:*D", serverId: "1", serverName: "Роутер", name: "Отдельный сертификат", kind: "cert", certName: "ipsec-user-psk", authMethod: "certificate", online: false, disabled: false, managed: true } + 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() === "DELETE") { expect(decodeURIComponent(path)).toBe("/api/ipsec/users/1/identity:*P"); deleted = true; return r.fulfill({ json: { ok: true } }) } + return r.fulfill({ json: path === "/api/ipsec" ? { servers: [{ serverId: "1", serverName: "Роутер", initialized: true, ike2Ready: true, natRuleManaged: false, clientsTotal: deleted ? 1 : 2, clientsOnline: 0 }], clients: [certificate,...(deleted ? [] : [{ ...certificate, id: "psk", rosId: "identity:*P", certName: undefined, name: "PSK клиент", kind: "psk", authMethod: "pre-shared-key" }])] } + : path === "/api/servers" ? [{ id: 1, name: "Роутер", host: "192.0.2.1", enabled: true, type: "exit-node", country: "RU", status: "online" }] : path === "/api/auth/config" ? { required: false } : {} }) + }) + await page.goto("/ipsec?server=1") + await page.locator("tr[data-row-id]").filter({ has: page.getByRole("button", { name: "PSK клиент", exact: true }) }).getByRole("button", { name: "Удалить клиента", exact: true }).click() + const dialog = page.getByRole("alertdialog") + await expect(dialog).toContainText("Сертификаты останутся на роутере") + await expect(dialog.getByRole("checkbox")).toHaveCount(0) + await dialog.getByRole("button", { name: "Удалить", exact: true }).click() + await expect(dialog).toHaveCount(0) + await expect(page.getByRole("button", { name: "PSK клиент", exact: true })).toHaveCount(0) + await expect(page.getByRole("button", { name: "Отдельный сертификат", exact: true })).toBeVisible() + }) +} + for (const width of [1280, 320]) for (const shared of [false, true]) { test(`IPsec: сохранить сертификат при удалении identity shared=${shared} ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 1000 })