From bd3f996f55cccd312dfe5861900849ca21791be8 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 8 Oct 2026 15:50:11 +0700 Subject: [PATCH] =?UTF-8?q?fix(gre):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B2=D0=BD=D1=83=D1=82=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20IP=20=D1=82=D1=83=D0=BD=D0=BD=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/gre/page.tsx | 2 +- backend/src/routes/gre.ts | 7 ++++--- backend/src/services/entity-snapshots.ts | 5 ++++- backend/src/services/gre-comment.test.ts | 26 ++++++++++++++++++++++++ backend/src/services/gre-comment.ts | 25 +++++++++++++++++++++++ backend/src/services/gre-live.ts | 7 ++++--- docs/ux-ui-external-acceptance.md | 10 +++++++++ docs/ux-ui-plan.md | 4 ++-- docs/ux-ui-progress.md | 14 +++++++++++-- docs/ux-ui-stage-12.md | 12 ++++++++++- docs/ux-ui-test-server.md | 10 +++++++++ tests/e2e/gre-form.spec.ts | 4 +++- 12 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 backend/src/services/gre-comment.test.ts create mode 100644 backend/src/services/gre-comment.ts diff --git a/app/(main)/gre/page.tsx b/app/(main)/gre/page.tsx index 52f06a00..7c5026b0 100644 --- a/app/(main)/gre/page.tsx +++ b/app/(main)/gre/page.tsx @@ -322,7 +322,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on localAddress: form.localAddress.trim() || undefined, remoteAddress: form.remoteAddress.trim(), localInnerIp: form.localInnerIp.trim() || undefined, - remoteInnerIp: form.remoteInnerIp.trim() || undefined, + remoteInnerIp: form.remoteInnerIp.trim(), comment: form.comment || undefined, enabled: form.enabled, mtu: form.mtu, diff --git a/backend/src/routes/gre.ts b/backend/src/routes/gre.ts index 99cf63fe..30dd5d78 100644 --- a/backend/src/routes/gre.ts +++ b/backend/src/routes/gre.ts @@ -24,6 +24,7 @@ import { type ConfigRevisionSource, } from "../services/config-revisions.js" import { parseDbServerId } from "../utils/server-id.js" +import { readGreComment, writeGreComment } from "../services/gre-comment.js" const TunnelWriteSchema = z.object({ serverId: z.string().min(1), @@ -86,8 +87,8 @@ function tunnelFromBody(body: z.infer & { keepaliveInt localAddress: body.localAddress ?? "", remoteAddress: body.remoteAddress, localInnerIp: body.localInnerIp ?? "", - remoteInnerIp: body.remoteInnerIp ?? "", - comment: body.comment ?? "", + remoteInnerIp: body.remoteInnerIp ?? readGreComment(body.comment).remoteInnerIp, + comment: body.remoteInnerIp === undefined ? body.comment ?? "" : writeGreComment(body.comment ?? "", body.remoteInnerIp), disabled: body.enabled === false, mtu: body.mtu ?? 1476, keepalive: keepalive ?? "0", @@ -149,7 +150,7 @@ const greRoutes: FastifyPluginAsyncZod = async (app) => { localAddress: body.localAddress ?? live.localAddress, remoteAddress: body.remoteAddress || live.remoteAddress, localInnerIp: body.localInnerIp ?? state.addrs.find((a) => a.interfaceName === live.name)?.address ?? "", - remoteInnerIp: body.remoteInnerIp, + remoteInnerIp: body.remoteInnerIp ?? state.tunnels.find(t => t.name === live.name)?.remoteInnerIp ?? "", comment: body.comment ?? live.comment, enabled: body.enabled ?? !live.disabled, mtu: body.mtu ?? live.mtu, diff --git a/backend/src/services/entity-snapshots.ts b/backend/src/services/entity-snapshots.ts index d3d724ec..1c3ae9f4 100644 --- a/backend/src/services/entity-snapshots.ts +++ b/backend/src/services/entity-snapshots.ts @@ -1,5 +1,6 @@ /** Канонические снапшоты и планы restore для firewall / WireGuard / GRE / IPsec. */ import { isIpsecManagedComment } from "./ipsec-config.js" +import { readGreComment, writeGreComment } from "./gre-comment.js" export type FirewallFamily = "ip" | "ip6" export type FirewallTable = "filter" | "nat" | "mangle" | "raw" @@ -854,6 +855,8 @@ export function parseGreSnapshot(payload: unknown): GreSnapshot { } export function greInterfaceBody(t: GreSnapshotTunnel): Record { + const stored = readGreComment(t.comment) + const peer = t.remoteInnerIp || (stored.comment === t.comment ? stored.remoteInnerIp : "") return compactBody({ name: t.name, "local-address": t.localAddress && t.localAddress !== "0.0.0.0" ? t.localAddress : undefined, @@ -863,7 +866,7 @@ export function greInterfaceBody(t: GreSnapshotTunnel): Record { dscp: t.dscp, "clamp-tcp-mss": t.clampTcpMss ? "yes" : "no", "allow-fast-path": t.allowFastPath ? "yes" : "no", - comment: t.comment, + comment: writeGreComment(t.comment, peer), disabled: rosYesNo(t.disabled), "ipsec-secret": isHiddenSecret(t.ipsecSecret) ? undefined : t.ipsecSecret, }) diff --git a/backend/src/services/gre-comment.test.ts b/backend/src/services/gre-comment.test.ts new file mode 100644 index 00000000..9fa947b7 --- /dev/null +++ b/backend/src/services/gre-comment.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict" +import {readGreComment,writeGreComment} from "./gre-comment.js" +import {canonicalGreSnapshot,planGreCreate,planGreRestore} from "./entity-snapshots.js" +import {mapGreLive} from "./gre-live.js" +const comment="Тестовый туннель\nВторая строка",peer="198.18.255.250/30" +const raw=writeGreComment(comment,peer) +assert.deepEqual(readGreComment(raw),{comment,remoteInnerIp:peer}) +assert.equal(writeGreComment(raw,peer),raw) +assert.deepEqual(readGreComment("gateway=10.0.0.2/30"),{comment:"gateway=10.0.0.2/30",remoteInnerIp:"10.0.0.2/30"}) +assert.equal(readGreComment("note\n[mmapp-gre-peer:v1:bad]").comment,"note\n[mmapp-gre-peer:v1:bad]") +const desired=canonicalGreSnapshot({tunnels:[{name:"gre-test",localInnerIp:"198.18.255.249/30",remoteInnerIp:peer,comment}]}) +const create=planGreCreate(desired.tunnels[0]);assert.deepEqual(create.map(x=>x.path),["/interface/gre","/ip/address"]) +const native={".id":"*3",name:"gre-test","local-address":"194.226.121.150","remote-address":"104.165.244.7",comment:(create[0] as {body:Record}).body.comment,mtu:"1400",disabled:"false",running:"true"} +const server={id:1} as Parameters[0] +const live=mapGreLive(server,[native],[{".id":"*4",address:"198.18.255.249/30",interface:"gre-test"}]) +assert.equal(live.tunnels[0].remoteInnerIp,peer);assert.equal(live.tunnels[0].comment,comment);assert.equal(live.snapshot.tunnels[0].remoteInnerIp,peer) +const restored=planGreRestore(live.snapshot,{gre:live.gre,addrs:live.addrs});const iface=restored.find(x=>x.op==="patch"&&x.path==="/interface/gre/*3") as {body:Record};assert(iface);assert.equal(readGreComment(iface.body.comment).remoteInnerIp,peer) +assert(restored.every(x=>!x.path.startsWith("/ip/route"))) +const clearSnapshot=canonicalGreSnapshot({tunnels:[{...live.snapshot.tunnels[0],remoteInnerIp:""}]}) +const clearedRestore=planGreRestore(clearSnapshot,{gre:live.gre,addrs:live.addrs}).find(x=>x.op==="patch"&&x.path==="/interface/gre/*3") as {body:Record} +assert.equal(readGreComment(clearedRestore.body.comment).remoteInnerIp,"") +const legacy=canonicalGreSnapshot({tunnels:[{name:"legacy",comment:"gateway=10.0.0.2/30",remoteInnerIp:""}]});assert.equal(readGreComment((planGreCreate(legacy.tunnels[0])[0] as {body:Record}).body.comment).remoteInnerIp,"10.0.0.2/30") +const cleared=writeGreComment("gateway=10.0.0.2/30","");assert.equal(readGreComment(cleared).remoteInnerIp,"") +const clearPlan=planGreCreate(canonicalGreSnapshot({tunnels:[{name:"clear",comment:cleared,remoteInnerIp:""}]}).tunnels[0]);assert.equal(readGreComment((clearPlan[0] as {body:Record}).body.comment).remoteInnerIp,"") +assert.equal(readGreComment(writeGreComment(raw,"")).comment,comment) +console.log("GRE peer metadata: native/read/restore/legacy/clear, no route operations") diff --git a/backend/src/services/gre-comment.ts b/backend/src/services/gre-comment.ts new file mode 100644 index 00000000..7ae894d7 --- /dev/null +++ b/backend/src/services/gre-comment.ts @@ -0,0 +1,25 @@ +function legacyPeer(comment: string): string { + return comment.match(/(?:network|gateway)\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? "" +} + +/** Keep app-only peer information on RouterOS without changing routes. */ +export function readGreComment(raw = ""): { comment: string; remoteInnerIp: string } { + const match = raw.match(/(?:\n)?\[mmapp-gre-peer:v1:([A-Za-z0-9_-]+)\]$/) + if (match) { + try { + const value: unknown = JSON.parse(Buffer.from(match[1], "base64url").toString("utf8")) + if (value && typeof value === "object" && "remoteInnerIp" in value && typeof value.remoteInnerIp === "string") { + return { comment: raw.slice(0, match.index), remoteInnerIp: value.remoteInnerIp } + } + } catch { /* Preserve unrecognized user comments verbatim. */ } + } + return { comment: raw, remoteInnerIp: legacyPeer(raw) } +} + +export function writeGreComment(raw: string, remoteInnerIp: string): string { + const { comment } = readGreComment(raw) + const peer = remoteInnerIp.trim() + if (!peer && !legacyPeer(comment)) return comment + const payload = Buffer.from(JSON.stringify({ remoteInnerIp: peer }), "utf8").toString("base64url") + return `${comment}${comment ? "\n" : ""}[mmapp-gre-peer:v1:${payload}]` +} diff --git a/backend/src/services/gre-live.ts b/backend/src/services/gre-live.ts index 40af0e74..0a886940 100644 --- a/backend/src/services/gre-live.ts +++ b/backend/src/services/gre-live.ts @@ -2,6 +2,7 @@ import { eq } from "drizzle-orm" import { db } from "../db/index.js" import { servers } from "../db/schema.js" import { MikrotikClient } from "./mikrotik.js" +import { readGreComment } from "./gre-comment.js" import { canonicalGreSnapshot, type GreLiveAddr, @@ -83,7 +84,7 @@ function parseDscp(value: string | undefined): "inherit" | number { function parseInnerFromComment(comment: string | undefined): { localInnerIp: string; remoteInnerIp: string } { if (!comment) return { localInnerIp: "", remoteInnerIp: "" } const local = comment.match(/address\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? "" - const remote = comment.match(/(?:network|gateway)\s*=\s*([0-9.]+\/\d+)/)?.[1] ?? "" + const remote = readGreComment(comment).remoteInnerIp return { localInnerIp: local, remoteInnerIp: remote } } @@ -164,7 +165,7 @@ export function mapGreLive( dscp: parseDscp(g.dscp), clampTcpMss: g["clamp-tcp-mss"] !== "false" && g["clamp-tcp-mss"] !== "no", allowFastPath: g["allow-fast-path"] !== "false" && g["allow-fast-path"] !== "no", - comment: g.comment ?? "", + comment: readGreComment(g.comment).comment, enabled: !disabled, status: disabled ? "down" : running ? "up" : "degraded", }) @@ -178,7 +179,7 @@ export function mapGreLive( localAddress: g.localAddress, remoteAddress: g.remoteAddress, localInnerIp: addrs.find((a) => a.interfaceName === g.name)?.address ?? "", - remoteInnerIp: "", + remoteInnerIp: tunnels.find(t => t.name === g.name)?.remoteInnerIp ?? "", comment: g.comment, disabled: g.disabled, mtu: g.mtu, diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index be205fbe..4cf019c5 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -355,3 +355,13 @@ Retry POST0, Telegram calls0, JS errors0, SQL fixture counts сохраняли Обнаружен настоящий дефект: принятый remoteInnerIp возвращается пустым, затем теряется в форме редактирования. Связность обеспечивается настройкой обоих реальных /ip/address, но сохранение формы этим не считается исправленным. Следующий12.5.52 — исправить metadata persistence/restore peer IP и профильные тесты, затем развернуть и повторить настоящий GRE цикл. Ограничения ROS6 для остальных VPN/прав/частичных отказов остаются. Control main2servers/9speed/retention14 сохранён; application GRE revisions намеренно остаются журналом выполненных действий. Временный контейнер/туннель8018 удалены; RouterOS тестовые объекты очищены. Локальный коммит без push. Официальная схема пары и keepalive: [MikroTik GRE](https://manual.mikrotik.com/docs/virtual-private-networks/gre/). Документация текущей версии проверена, реальные команды/ответы — RouterOS7.24.5. + +## 12.5.52 — сохранение удалённого внутреннего IP GRE (2026-10-08) + +Исправлен дефект, обнаруженный на настоящей GRE паре в12.5.51. Новый backend services/gre-comment.ts хранит remoteInnerIp в версионированном base64url JSON suffix комментария RouterOS. Это метаданные приложения, не маршруты и не /ip/address на удалённом устройстве. greInterfaceBody сохраняет peer, mapGreLive возвращает его и чистый пользовательский комментарий; snapshot содержит peer и исходный raw comment для корректного restore. PATCH без remoteInnerIp сохраняет текущий peer; явная пустая строка очищает. Форма теперь передаёт пустую строку при очистке вместо пропуска поля. + +Старые gateway/network комментарии читаются и сохраняются при restore; непонятный/повреждённый suffix остаётся обычным пользовательским текстом. Явная очистка legacy peer записывает пустое metadata значение, чтобы прежняя подсказка комментария не возвращала удалённое поле. Метаданные не создают сетевые маршруты и не содержат PSK/паролей; GRE/IPsec secret logic не менялась. Raw metadata видна в прямом RouterOS comment, в API/UI служебный suffix отделён от пользовательского текста. + +gre-comment.test.ts проверил create plan→native comment→map→snapshot→restore, Unicode/несколько строк, idempotence, legacy, invalid suffix и explicit clear; планы ограничены GRE/IP, без /ip/route. Прежний entity-snapshots.test.ts passed. Playwright gre-form+gre-toggle5/5: создание/DTO/edit, сохранённый ввод при отказе и неверном receipt, explicit empty remoteInnerIp, подтверждение переключения. Browser ответы локальных проверок подменены; новое metadata на реальном RouterOS этим блоком не принято. Backend tsc --noEmit и scoped frontend typecheck passed. Eslint без ошибок, одно прежнее unused bool предупреждение в entity-snapshots.ts:258; дифф не меняет этот helper. + +Код и тесты готовы к сборке; удалённый runtime пока backend8782054/frontendb55588f, реальные GRE объекты из51 удалены. Следующий12.5.53 — production Docker build/deploy backend и frontend, новая собственная GRE пара с независимой проверкой peer read/MTU-toggle/clear/restore, реальный UI и двусторонний Ping, очистка по имени и decoded ownership comment. Пользовательское требование реальных VPN объектов сохраняется. Локальный коммит без push. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 53350dac..ce36da4f 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-08: 12.5.51 создал реальную GRE пару через production API, проверил MTU/enable/disable, двусторонний Ping3/3 и реальные UI формы/URL/320px. Обе стороны удалены, выбранная конфигурация восстановлена. Подтверждён дефект: remoteInnerIp теряется в receipt и форме. Следующий12.5.52 — исправить сохранение peer IP; runtime backend8782054/frontendb55588f прежний. При VPN приёмке создавать собственные реальные тестовые объекты, особенно GRE, и удалять их после проверки. +Актуальное продолжение 2026-10-08: 12.5.52 локально исправил потерю remoteInnerIp: versioned metadata комментария GRE, чистый пользовательский comment в DTO, сохранение peer в snapshot/restore и PATCH, явная очистка из формы. Backend pipeline tests/entity snapshots и browser5/5, backend/frontend typecheck passed; lint без ошибок (одно прежнее предупреждение). Исправление ещё не развёрнуто: runtime backend8782054/frontendb55588f. Следующий12.5.53 — собрать/развернуть обе части и повторить реальный GRE lifecycle/форму/очистку с metadata. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-08 -Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend8782054/frontendb55588f и два 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 — исправление сохранения metadata. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения. +Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend8782054/frontendb55588f и два 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 — build/deploy и повтор настоящей GRE приёмки. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения. Выполненные внешние сценарии не заменяют приёмку оставшихся интеграций и условий эксплуатации. Датированные продолжения ниже — история; точный следующий шаг указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 1e4f4dfd..27334b00 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-08: 12.5.51 создал реальную GRE пару через production API, проверил MTU/enable/disable, двусторонний Ping3/3 и реальные UI формы/URL/320px. Обе стороны удалены, выбранная конфигурация восстановлена. Подтверждён дефект: remoteInnerIp теряется в receipt и форме. Следующий12.5.52 — исправить сохранение peer IP; runtime backend8782054/frontendb55588f прежний. При VPN приёмке создавать собственные реальные тестовые объекты, особенно GRE, и удалять их после проверки. +Актуальное продолжение 2026-10-08: 12.5.52 локально исправил потерю remoteInnerIp: versioned metadata комментария GRE, чистый пользовательский comment в DTO, сохранение peer в snapshot/restore и PATCH, явная очистка из формы. Backend pipeline tests/entity snapshots и browser5/5, backend/frontend typecheck passed; lint без ошибок (одно прежнее предупреждение). Исправление ещё не развёрнуто: runtime backend8782054/frontendb55588f. Следующий12.5.53 — собрать/развернуть обе части и повторить реальный GRE lifecycle/форму/очистку с metadata. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -34,7 +34,17 @@ | 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–51 и ограничения ниже. Runtime backend8782054/frontendb55588f. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–52 и ограничения ниже. Runtime backend8782054/frontendb55588f. | + +## 12.5.52 — сохранение удалённого внутреннего IP GRE (2026-10-08) + +Исправлен дефект, обнаруженный на настоящей GRE паре в12.5.51. Новый backend services/gre-comment.ts хранит remoteInnerIp в версионированном base64url JSON suffix комментария RouterOS. Это метаданные приложения, не маршруты и не /ip/address на удалённом устройстве. greInterfaceBody сохраняет peer, mapGreLive возвращает его и чистый пользовательский комментарий; snapshot содержит peer и исходный raw comment для корректного restore. PATCH без remoteInnerIp сохраняет текущий peer; явная пустая строка очищает. Форма теперь передаёт пустую строку при очистке вместо пропуска поля. + +Старые gateway/network комментарии читаются и сохраняются при restore; непонятный/повреждённый suffix остаётся обычным пользовательским текстом. Явная очистка legacy peer записывает пустое metadata значение, чтобы прежняя подсказка комментария не возвращала удалённое поле. Метаданные не создают сетевые маршруты и не содержат PSK/паролей; GRE/IPsec secret logic не менялась. Raw metadata видна в прямом RouterOS comment, в API/UI служебный suffix отделён от пользовательского текста. + +gre-comment.test.ts проверил create plan→native comment→map→snapshot→restore, Unicode/несколько строк, idempotence, legacy, invalid suffix и explicit clear; планы ограничены GRE/IP, без /ip/route. Прежний entity-snapshots.test.ts passed. Playwright gre-form+gre-toggle5/5: создание/DTO/edit, сохранённый ввод при отказе и неверном receipt, explicit empty remoteInnerIp, подтверждение переключения. Browser ответы локальных проверок подменены; новое metadata на реальном RouterOS этим блоком не принято. Backend tsc --noEmit и scoped frontend typecheck passed. Eslint без ошибок, одно прежнее unused bool предупреждение в entity-snapshots.ts:258; дифф не меняет этот helper. + +Код и тесты готовы к сборке; удалённый runtime пока backend8782054/frontendb55588f, реальные GRE объекты из51 удалены. Следующий12.5.53 — production Docker build/deploy backend и frontend, новая собственная GRE пара с независимой проверкой peer read/MTU-toggle/clear/restore, реальный UI и двусторонний Ping, очистка по имени и decoded ownership comment. Пользовательское требование реальных VPN объектов сохраняется. Локальный коммит без push. ## 12.5.51 — реальная GRE пара, связность и lifecycle (2026-10-08) diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index a3a984a6..6f7e9e58 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-08: 12.5.51 создал реальную GRE пару через production API, проверил MTU/enable/disable, двусторонний Ping3/3 и реальные UI формы/URL/320px. Обе стороны удалены, выбранная конфигурация восстановлена. Подтверждён дефект: remoteInnerIp теряется в receipt и форме. Следующий12.5.52 — исправить сохранение peer IP; runtime backend8782054/frontendb55588f прежний. При VPN приёмке создавать собственные реальные тестовые объекты, особенно GRE, и удалять их после проверки. +Актуальное продолжение 2026-10-08: 12.5.52 локально исправил потерю remoteInnerIp: versioned metadata комментария GRE, чистый пользовательский comment в DTO, сохранение peer в snapshot/restore и PATCH, явная очистка из формы. Backend pipeline tests/entity snapshots и browser5/5, backend/frontend typecheck passed; lint без ошибок (одно прежнее предупреждение). Исправление ещё не развёрнуто: runtime backend8782054/frontendb55588f. Следующий12.5.53 — собрать/развернуть обе части и повторить реальный GRE lifecycle/форму/очистку с metadata. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -1520,3 +1520,13 @@ Retry POST0, Telegram calls0, JS errors0, SQL fixture counts сохраняли Обнаружен настоящий дефект: принятый remoteInnerIp возвращается пустым, затем теряется в форме редактирования. Связность обеспечивается настройкой обоих реальных /ip/address, но сохранение формы этим не считается исправленным. Следующий12.5.52 — исправить metadata persistence/restore peer IP и профильные тесты, затем развернуть и повторить настоящий GRE цикл. Ограничения ROS6 для остальных VPN/прав/частичных отказов остаются. Control main2servers/9speed/retention14 сохранён; application GRE revisions намеренно остаются журналом выполненных действий. Временный контейнер/туннель8018 удалены; RouterOS тестовые объекты очищены. Локальный коммит без push. Официальная схема пары и keepalive: [MikroTik GRE](https://manual.mikrotik.com/docs/virtual-private-networks/gre/). Документация текущей версии проверена, реальные команды/ответы — RouterOS7.24.5. + +## 12.5.52 — сохранение удалённого внутреннего IP GRE (2026-10-08) + +Исправлен дефект, обнаруженный на настоящей GRE паре в12.5.51. Новый backend services/gre-comment.ts хранит remoteInnerIp в версионированном base64url JSON suffix комментария RouterOS. Это метаданные приложения, не маршруты и не /ip/address на удалённом устройстве. greInterfaceBody сохраняет peer, mapGreLive возвращает его и чистый пользовательский комментарий; snapshot содержит peer и исходный raw comment для корректного restore. PATCH без remoteInnerIp сохраняет текущий peer; явная пустая строка очищает. Форма теперь передаёт пустую строку при очистке вместо пропуска поля. + +Старые gateway/network комментарии читаются и сохраняются при restore; непонятный/повреждённый suffix остаётся обычным пользовательским текстом. Явная очистка legacy peer записывает пустое metadata значение, чтобы прежняя подсказка комментария не возвращала удалённое поле. Метаданные не создают сетевые маршруты и не содержат PSK/паролей; GRE/IPsec secret logic не менялась. Raw metadata видна в прямом RouterOS comment, в API/UI служебный suffix отделён от пользовательского текста. + +gre-comment.test.ts проверил create plan→native comment→map→snapshot→restore, Unicode/несколько строк, idempotence, legacy, invalid suffix и explicit clear; планы ограничены GRE/IP, без /ip/route. Прежний entity-snapshots.test.ts passed. Playwright gre-form+gre-toggle5/5: создание/DTO/edit, сохранённый ввод при отказе и неверном receipt, explicit empty remoteInnerIp, подтверждение переключения. Browser ответы локальных проверок подменены; новое metadata на реальном RouterOS этим блоком не принято. Backend tsc --noEmit и scoped frontend typecheck passed. Eslint без ошибок, одно прежнее unused bool предупреждение в entity-snapshots.ts:258; дифф не меняет этот helper. + +Код и тесты готовы к сборке; удалённый runtime пока backend8782054/frontendb55588f, реальные GRE объекты из51 удалены. Следующий12.5.53 — production Docker build/deploy backend и frontend, новая собственная GRE пара с независимой проверкой peer read/MTU-toggle/clear/restore, реальный UI и двусторонний Ping, очистка по имени и decoded ownership comment. Пользовательское требование реальных VPN объектов сохраняется. Локальный коммит без push. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index e5958c4c..9a6fd7c6 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -363,3 +363,13 @@ Retry POST0, Telegram calls0, JS errors0, SQL fixture counts сохраняли Обнаружен настоящий дефект: принятый remoteInnerIp возвращается пустым, затем теряется в форме редактирования. Связность обеспечивается настройкой обоих реальных /ip/address, но сохранение формы этим не считается исправленным. Следующий12.5.52 — исправить metadata persistence/restore peer IP и профильные тесты, затем развернуть и повторить настоящий GRE цикл. Ограничения ROS6 для остальных VPN/прав/частичных отказов остаются. Control main2servers/9speed/retention14 сохранён; application GRE revisions намеренно остаются журналом выполненных действий. Временный контейнер/туннель8018 удалены; RouterOS тестовые объекты очищены. Локальный коммит без push. Официальная схема пары и keepalive: [MikroTik GRE](https://manual.mikrotik.com/docs/virtual-private-networks/gre/). Документация текущей версии проверена, реальные команды/ответы — RouterOS7.24.5. + +## 12.5.52 — сохранение удалённого внутреннего IP GRE (2026-10-08) + +Исправлен дефект, обнаруженный на настоящей GRE паре в12.5.51. Новый backend services/gre-comment.ts хранит remoteInnerIp в версионированном base64url JSON suffix комментария RouterOS. Это метаданные приложения, не маршруты и не /ip/address на удалённом устройстве. greInterfaceBody сохраняет peer, mapGreLive возвращает его и чистый пользовательский комментарий; snapshot содержит peer и исходный raw comment для корректного restore. PATCH без remoteInnerIp сохраняет текущий peer; явная пустая строка очищает. Форма теперь передаёт пустую строку при очистке вместо пропуска поля. + +Старые gateway/network комментарии читаются и сохраняются при restore; непонятный/повреждённый suffix остаётся обычным пользовательским текстом. Явная очистка legacy peer записывает пустое metadata значение, чтобы прежняя подсказка комментария не возвращала удалённое поле. Метаданные не создают сетевые маршруты и не содержат PSK/паролей; GRE/IPsec secret logic не менялась. Raw metadata видна в прямом RouterOS comment, в API/UI служебный suffix отделён от пользовательского текста. + +gre-comment.test.ts проверил create plan→native comment→map→snapshot→restore, Unicode/несколько строк, idempotence, legacy, invalid suffix и explicit clear; планы ограничены GRE/IP, без /ip/route. Прежний entity-snapshots.test.ts passed. Playwright gre-form+gre-toggle5/5: создание/DTO/edit, сохранённый ввод при отказе и неверном receipt, explicit empty remoteInnerIp, подтверждение переключения. Browser ответы локальных проверок подменены; новое metadata на реальном RouterOS этим блоком не принято. Backend tsc --noEmit и scoped frontend typecheck passed. Eslint без ошибок, одно прежнее unused bool предупреждение в entity-snapshots.ts:258; дифф не меняет этот helper. + +Код и тесты готовы к сборке; удалённый runtime пока backend8782054/frontendb55588f, реальные GRE объекты из51 удалены. Следующий12.5.53 — production Docker build/deploy backend и frontend, новая собственная GRE пара с независимой проверкой peer read/MTU-toggle/clear/restore, реальный UI и двусторонний Ping, очистка по имени и decoded ownership comment. Пользовательское требование реальных VPN объектов сохраняется. Локальный коммит без push. diff --git a/tests/e2e/gre-form.spec.ts b/tests/e2e/gre-form.spec.ts index 73bc9ef1..f33529bd 100644 --- a/tests/e2e/gre-form.spec.ts +++ b/tests/e2e/gre-form.spec.ts @@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test" for (const mode of ["create", "dto", "edit"]) { test(`GRE: сохранение ${mode} подтверждается API`, async ({ page }) => { let attempts = 0 + const remoteInnerIp = mode === "edit" ? "" : "10.0.0.2/30" let release: (() => void) | undefined const gate = new Promise(resolve => { release = resolve }) const tunnel = { id: "*T/1", name: "gre-test", serverId: "1", localAddress: "192.0.2.1", remoteAddress: "192.0.2.2", localInnerIp: "10.0.0.1/30", remoteInnerIp: "10.0.0.2/30", poolId: "pool", ipsec: null, mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 3, dscp: "inherit", clampTcpMss: true, allowFastPath: true, comment: "", enabled: true, status: "up" } @@ -12,7 +13,7 @@ for (const mode of ["create", "dto", "edit"]) { if (route.request().method() === (mode === "edit" ? "PATCH" : "POST")) { attempts++ expect(path).toBe("/api/gre/tunnels") - expect(route.request().postDataJSON()).toMatchObject({ serverId: "1", name: "gre-test", remoteAddress: "192.0.2.9", comment: "сохранённый ввод" }) + expect(route.request().postDataJSON()).toMatchObject({ serverId: "1", name: "gre-test", remoteAddress: "192.0.2.9", remoteInnerIp, comment: "сохранённый ввод" }) if (mode === "edit") expect(route.request().postDataJSON().rosId).toBe("*T/1") if (attempts === 1) { await gate; return route.fulfill({ status: 403, json: { error: "Запись запрещена тест" } }) } if (attempts === 2) return route.fulfill({ json: {} }) @@ -38,6 +39,7 @@ for (const mode of ["create", "dto", "edit"]) { } await sheet.getByLabel("Удалённый адрес").fill("192.0.2.9") await sheet.getByLabel("Комментарий").fill("сохранённый ввод") + await sheet.getByLabel("Удалённый IP", { exact: true }).fill(remoteInnerIp) const submit = sheet.getByRole("button", { name: mode === "edit" ? "Сохранить" : "Создать туннель", exact: true }) await submit.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() }) await expect.poll(() => attempts).toBe(1)