From e2d81f9657d02ef159d3dfc9e88f6a28e2738003 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 9 Oct 2026 10:38:24 +0700 Subject: [PATCH] =?UTF-8?q?fix(vxlan):=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B4=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B8=20=D1=8D=D0=BA=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/vxlan/page.tsx | 2 +- backend/package.json | 2 +- backend/src/routes/vxlan.test.ts | 34 +++++++ backend/src/routes/vxlan.ts | 47 +++++---- backend/src/services/vxlan-live.test.ts | 27 +++++- backend/src/services/vxlan-live.ts | 68 +++++++++---- components/data-grids/vxlan-data-grid.tsx | 88 ++++++++++------- docs/ux-ui-external-acceptance.md | 36 ++++++- docs/ux-ui-plan.md | 46 ++++++++- docs/ux-ui-progress.md | 40 +++++++- docs/ux-ui-stage-12.md | 36 ++++++- docs/ux-ui-test-server.md | 38 +++++++- .../browser-vxlan-section-2026-10-09.json | 57 +++++++++++ ...ploy-browser-vxlan-section-2026-10-09.json | 40 ++++++++ .../deploy-vxlan-section-2026-10-09.json | 37 +++++++ .../vxlan-section-native-2026-10-09.json | 97 +++++++++++++++++++ ...an-section-source-manifest-2026-10-09.json | 54 +++++++++++ lib/data.ts | 1 + lib/vxlan-export.test.ts | 3 + lib/vxlan-export.ts | 4 +- lib/vxlan-response.ts | 1 + tests/e2e/vxlan-links.spec.ts | 17 ++++ tests/e2e/vxlan-table.spec.ts | 3 +- 23 files changed, 689 insertions(+), 89 deletions(-) create mode 100644 backend/src/routes/vxlan.test.ts create mode 100644 docs/ux-ui/acceptance/browser-vxlan-section-2026-10-09.json create mode 100644 docs/ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json create mode 100644 docs/ux-ui/acceptance/deploy-vxlan-section-2026-10-09.json create mode 100644 docs/ux-ui/acceptance/vxlan-section-native-2026-10-09.json create mode 100644 docs/ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json diff --git a/app/(main)/vxlan/page.tsx b/app/(main)/vxlan/page.tsx index 71f80cb2..c58d8554 100644 --- a/app/(main)/vxlan/page.tsx +++ b/app/(main)/vxlan/page.tsx @@ -357,7 +357,7 @@ function VxlanPageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, -

Экспорт содержит интерфейс и статические VTEP выбранного туннеля. Bridge и FDB не входят в доступный снимок. Параметры VXLAN в документации MikroTik

+

Экспорт содержит интерфейс, известный MTU и статические VTEP выбранного туннеля. IP-адреса, bridge и FDB в него не входят. Параметры VXLAN в документации MikroTik

diff --git a/backend/package.json b/backend/package.json index 558f0c45..60dbbe29 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,7 @@ "test:ros-file": "tsx src/services/ros-file-contents.test.ts", "test:scheduler": "tsx src/services/scheduler-running.test.ts && tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts", "test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts", - "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts", + "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/routes/vxlan.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts", "test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution && npm run test:system-database", "test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts", "test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts", diff --git a/backend/src/routes/vxlan.test.ts b/backend/src/routes/vxlan.test.ts new file mode 100644 index 00000000..c66ba673 --- /dev/null +++ b/backend/src/routes/vxlan.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict" +import Fastify from "fastify" +import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod" +import { createVxlanRoutes } from "./vxlan.js" +import type { servers } from "../db/schema.js" + +const devices = [ + { id: 1, enabled: true, type: "home-router" }, + { id: 2, enabled: false, type: "home-router" }, + { id: 3, enabled: false, type: "exit-node" }, +] as typeof servers.$inferSelect[] +let reads = 0, fail = false +const app = Fastify() +app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler) +await app.register(createVxlanRoutes({ + listResult: async () => ({ tunnels: [], failures: [] }), + findServer: async id => devices.find(device => device.id === id), + readServer: async () => { reads++; if (fail) throw new Error("offline"); return [] }, +}), { prefix: "/api" }) +for (const id of [2, 3]) { + const response = await app.inject({ method: "GET", url: `/api/servers/${id}/vxlan` }) + assert.equal(response.statusCode, 409); assert.match(response.json().error, /выключен/) +} +assert.equal(reads, 0) +assert.equal((await app.inject({ method: "GET", url: "/api/servers/999/vxlan" })).statusCode, 404) +assert.equal((await app.inject({ method: "GET", url: "/api/servers/0/vxlan" })).statusCode, 400) +assert.equal(reads, 0) +const enabled = await app.inject({ method: "GET", url: "/api/servers/1/vxlan" }) +assert.equal(enabled.statusCode, 200); assert.deepEqual(enabled.json(), { tunnels: [] }); assert.equal(reads, 1) +fail = true +assert.equal((await app.inject({ method: "GET", url: "/api/servers/1/vxlan" })).statusCode, 502) +assert.equal((await app.inject({ method: "GET", url: "/api/vxlan" })).statusCode, 200) +await app.close() +console.log("VXLAN HTTP: disabled home/exit no poll, invalid/missing, empty and upstream failure passed") diff --git a/backend/src/routes/vxlan.ts b/backend/src/routes/vxlan.ts index bcdb5e85..2e2b3828 100644 --- a/backend/src/routes/vxlan.ts +++ b/backend/src/routes/vxlan.ts @@ -5,22 +5,35 @@ import { servers } from "../db/schema.js" import { listVxlanResult, listVxlanTunnelsForServer } from "../services/vxlan-live.js" import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js" -const vxlanRoutes: FastifyPluginAsyncZod = async (app) => { - app.get("/vxlan", async (_req, reply) => { - return reply.send(await listVxlanResult()) - }) - - app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => { - const params = req.params as ServerIdParams - const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0] - if (!server) return reply.status(404).send({ error: "Server not found" }) - try { - const tunnels = await listVxlanTunnelsForServer(server) - return reply.send({ tunnels }) - } catch { - return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" }) - } - }) +type ServerRow = typeof servers.$inferSelect +interface VxlanDependencies { + listResult: typeof listVxlanResult + findServer: (id: number) => Promise + readServer: typeof listVxlanTunnelsForServer } +const defaults: VxlanDependencies = { + listResult: listVxlanResult, + findServer: async id => (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0], + readServer: listVxlanTunnelsForServer, +} +export function createVxlanRoutes(dependencies: VxlanDependencies = defaults): FastifyPluginAsyncZod { + return async (app) => { + app.get("/vxlan", async (_req, reply) => { + return reply.send(await dependencies.listResult()) + }) -export default vxlanRoutes + app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => { + const params = req.params as ServerIdParams + const server = await dependencies.findServer(params.id) + if (!server) return reply.status(404).send({ error: "Сервер не найден" }) + if (!server.enabled) return reply.status(409).send({ error: "Сервер выключен. Опрос VXLAN не выполнялся" }) + try { + const tunnels = await dependencies.readServer(server) + return reply.send({ tunnels }) + } catch { + return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" }) + } + }) + } +} +export default createVxlanRoutes() diff --git a/backend/src/services/vxlan-live.test.ts b/backend/src/services/vxlan-live.test.ts index ce5b8132..43716f13 100644 --- a/backend/src/services/vxlan-live.test.ts +++ b/backend/src/services/vxlan-live.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict" -import { mapVxlanRow, pollVxlanServers } from "./vxlan-live.js" +import { mapVxlanRow, mapVxlanSnapshot, pollVxlanServers, listVxlanTunnelsForServer } from "./vxlan-live.js" const server = { id: 7, @@ -20,6 +20,7 @@ const row = mapVxlanRow( running: "true", disabled: "false", l2mtu: "1500", + mtu: "1400", "mac-learning": "true", "arp-proxy": "true", comment: "overlay", @@ -55,12 +56,32 @@ assert.equal(failed.tunnels.length, 0) assert.equal(failed.failures.length, 1) console.log("vxlan polling: mixed/empty/all-failed ok") +const complete = { ".id": "*4", name: "vx", vni: "10010", port: "4789", l2mtu: "65535", mtu: "1400", disabled: "false", running: "true", arp: "enabled", learning: "yes" } for (const arp of ["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"] as const) { - const mapped = mapVxlanRow(server, { ".id": "*4", name: "vx", arp, learning: "no" }, [], 0) + const mapped = mapVxlanRow(server, { ...complete, arp, learning: "no" }, [], 0) assert.equal(mapped.arpMode, arp) assert.equal(mapped.arpProxy, arp === "proxy-arp" || arp === "local-proxy-arp") assert.equal(mapped.macLearning, false) } console.log("vxlan: exact ARP modes and RouterOS no-learning passed") -assert.equal(mapVxlanRow(server, { arp: "enabled", "arp-proxy": "true" }, [], 0).arpProxy, false) +assert.equal(mapVxlanRow(server, { ...complete, arp: "enabled", "arp-proxy": "true" }, [], 0).arpProxy, false) + +for (const field of [".id", "name", "vni", "port", "l2mtu", "mtu", "disabled", "running", "arp", "learning"]) { + const incomplete = { ...complete } as Record + delete incomplete[field] + assert.throws(() => mapVxlanSnapshot(server, [incomplete], []), /Некорректный снимок/) +} +for (const bad of [{ vni: "12junk" }, { vni: "0" }, { port: "0" }, { port: "65536" }, { l2mtu: "unknown" }, { running: "unknown" }]) { + assert.throws(() => mapVxlanSnapshot(server, [{ ...complete, ...bad }], []), /Некорректный снимок/) +} +assert.deepEqual(mapVxlanSnapshot(server, [complete], [{ interface: "*4", "remote-ip": "192.0.2.2" }])[0].remoteVteps, ["192.0.2.2"]) +for (const vtep of [{ interface: "missing", "remote-ip": "192.0.2.2" }, { interface: "vx", "remote-ip": "invalid" }]) { + assert.throws(() => mapVxlanSnapshot(server, [complete], [vtep]), /статический VTEP/) +} +assert.throws(() => mapVxlanSnapshot(server, [complete, complete], []), /повторяющиеся/) +let disabledReads = 0 +assert.deepEqual(await pollVxlanServers([{ ...server, enabled: false }], async () => { disabledReads++; return [row] }), { tunnels: [], failures: [] }) +assert.equal(disabledReads, 0) +await assert.rejects(listVxlanTunnelsForServer({ ...server, enabled: false }), /выключен/) +console.log("VXLAN: incomplete snapshots, VTEP identities and disabled no-poll passed") diff --git a/backend/src/services/vxlan-live.ts b/backend/src/services/vxlan-live.ts index f15f0487..ce74e4b1 100644 --- a/backend/src/services/vxlan-live.ts +++ b/backend/src/services/vxlan-live.ts @@ -1,4 +1,5 @@ import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js" +import { isIP } from "node:net" import { eq } from "drizzle-orm" import { db } from "../db/index.js" import { servers } from "../db/schema.js" @@ -17,6 +18,7 @@ interface RosVxlan { disabled?: string comment?: string l2mtu?: string + mtu?: string arp?: string "arp-proxy"?: string "mac-learning"?: string @@ -40,6 +42,7 @@ export type VxlanTunnelLive = { vtepIp: string remoteVteps: string[] l2mtu: number + mtu: number arpProxy: boolean arpMode: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only" macLearning: boolean @@ -48,45 +51,55 @@ export type VxlanTunnelLive = { status: "up" | "down" } -function rosYes(v: string | undefined): boolean { - return v === "true" || v === "yes" +function rosYes(v: unknown): boolean { + return v === true || v === "true" || v === "yes" } -function parseIntSafe(v: string | undefined, fallback: number): number { - const n = Number.parseInt(v ?? "", 10) - return Number.isFinite(n) ? n : fallback +function readInteger(value: unknown, field: string, min: number, max: number): number { + const n = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : typeof value === "number" ? value : NaN + if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Некорректный снимок VXLAN: ${field}`) + return n +} + +function readFlag(value: unknown, field: string): boolean { + if (![true, false, "true", "false", "yes", "no"].includes(value as boolean | string)) throw new Error(`Некорректный снимок VXLAN: ${field}`) + return rosYes(value) } export function mapVxlanRow( server: ServerRow, vx: RosVxlan, vteps: RosVxlanVtep[], - idx: number, + _idx: number, ): VxlanTunnelLive { - const name = (vx.name ?? "").trim() || `vxlan-${idx + 1}` - const rosId = String(vx[".id"] ?? name) - const disabled = rosYes(vx.disabled) - const running = rosYes(vx.running) - const port = parseIntSafe(vx.port, 8472) - const arpMode: VxlanTunnelLive["arpMode"] = (["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"] as const).find(mode => mode === vx.arp) ?? (rosYes(vx["arp-proxy"]) ? "proxy-arp" : "enabled") + const name = typeof vx?.name === "string" ? vx.name.trim() : "" + const rosId = typeof vx?.[".id"] === "string" ? vx[".id"].trim() : "" + if (!name || !rosId) throw new Error("Некорректный снимок VXLAN: имя или ID интерфейса") + const disabled = readFlag(vx.disabled, "disabled") + const running = readFlag(vx.running, "running") + const port = readInteger(vx.port, "port", 1, 65535) + const explicitArp = (["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"] as const).find(mode => mode === vx.arp) + if (vx.arp !== undefined && !explicitArp) throw new Error("Некорректный снимок VXLAN: arp") + const arpMode: VxlanTunnelLive["arpMode"] = explicitArp ?? (readFlag(vx["arp-proxy"], "arp-proxy") ? "proxy-arp" : "enabled") const remoteVteps = vteps - .filter((v) => (v.interface ?? "").trim() === name) + .filter((v) => (v.interface ?? "").trim() === name || (v.interface ?? "").trim() === rosId) .map((v) => (v["remote-ip"] ?? "").trim()) .filter(Boolean) return { id: `${server.id}-${rosId}`, rosId, name, - vni: parseIntSafe(vx.vni, 0), + vni: readInteger(vx.vni, "vni", 1, 16777215), port: 0, dstPort: port, serverId: String(server.id), vtepIp: (vx["local-address"] ?? vx["vtep-address"] ?? "").trim(), remoteVteps, - l2mtu: parseIntSafe(vx.l2mtu, 1500), + l2mtu: readInteger(vx.l2mtu, "l2mtu", 1, 65535), + mtu: readInteger(vx.mtu, "mtu", 1, 65535), arpProxy: arpMode === "proxy-arp" || arpMode === "local-proxy-arp", arpMode, - macLearning: vx["mac-learning"] != null ? rosYes(vx["mac-learning"]) : vx.learning != null ? rosYes(vx.learning) : true, + macLearning: readFlag(vx["mac-learning"] ?? vx.learning, "learning"), comment: vx.comment ?? "", enabled: !disabled, status: !disabled && running ? "up" : "down", @@ -94,22 +107,37 @@ export function mapVxlanRow( } async function fetchVxlanForServer(server: ServerRow): Promise { + if (!server.enabled) throw new Error("Сервер выключен. Опрос VXLAN не выполнялся") const client = MikrotikClient.fromServer(server) const [vxRaw, vtepRaw] = await Promise.all([ client.get("/interface/vxlan"), client.get("/interface/vxlan/vteps"), ]) if (!Array.isArray(vxRaw) || !Array.isArray(vtepRaw)) throw new Error("Некорректный ответ VXLAN устройства") - const list = vxRaw - const vteps = vtepRaw - return list.map((vx, idx) => mapVxlanRow(server, vx, vteps, idx)) + return mapVxlanSnapshot(server, vxRaw, vtepRaw) +} + +export function mapVxlanSnapshot(server: ServerRow, list: RosVxlan[], vteps: RosVxlanVtep[]): VxlanTunnelLive[] { + if (vteps.some(vtep => !vtep || typeof vtep.interface !== "string" || !isIP(vtep["remote-ip"] ?? ""))) { + throw new Error("Некорректный снимок VXLAN: статический VTEP") + } + const mapped = list.map((vx, idx) => mapVxlanRow(server, vx, vteps, idx)) + if (new Set(mapped.map(row => row.rosId)).size !== mapped.length || new Set(mapped.map(row => row.name)).size !== mapped.length) { + throw new Error("Некорректный снимок VXLAN: повторяющиеся интерфейсы") + } + for (const vtep of vteps) { + if (!vtep || !mapped.some(row => row.name === vtep.interface || row.rosId === vtep.interface) || !isIP(vtep["remote-ip"] ?? "")) { + throw new Error("Некорректный снимок VXLAN: статический VTEP") + } + } + return mapped } export async function pollVxlanServers( serverRows: ServerRow[], fetchServer: (server: ServerRow) => Promise = fetchVxlanForServer, ): Promise<{ tunnels: VxlanTunnelLive[]; failures: Array<{ serverId: string; serverName: string; error: string }> }> { - const results = await Promise.all(serverRows.map(async server => { + const results = await Promise.all(serverRows.filter(server => server.enabled !== false).map(async server => { try { return { tunnels: await fetchServer(server), failures: [] } } catch (error) { return { tunnels: [], failures: [{ serverId: String(server.id), serverName: server.name, error: error instanceof Error ? error.message : String(error) }] } } })) diff --git a/components/data-grids/vxlan-data-grid.tsx b/components/data-grids/vxlan-data-grid.tsx index 0be0c235..eca0c440 100644 --- a/components/data-grids/vxlan-data-grid.tsx +++ b/components/data-grids/vxlan-data-grid.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useMemo } from "react" +import { createContext, useContext, useEffect, useMemo } from "react" import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" import { type ColumnDef, @@ -37,6 +37,43 @@ interface VxlanDataGridProps { onExport: (tunnel: VxlanTunnel) => void } +const VxlanActionsContext = createContext(null) +function VxlanActionsCell({ row }: { row: { original: VxlanTunnel } }) { + const onExport = useContext(VxlanActionsContext) + if (!onExport) throw new Error("VXLAN actions context is missing") + const tunnel = row.original + return ( +
+ + + + + } + /> + + onExport(tunnel)}> + + Экспорт .rsc + + + +
+ ) + +} + function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: VxlanDataGridProps) { const serverMap = useMemo( () => new Map(servers.map((s) => [s.id, s])), @@ -120,6 +157,18 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"), }, }, + { + id: "mtu", + size: 88, + accessorKey: "mtu", + header: ({ column }) => , + cell: ({ row }) => {row.original.mtu ?? "—"}, + meta: { + headerTitle: "MTU", + headerClassName: DATA_GRID_CELL_PAD, + cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"), + }, + }, { id: "remoteVteps", size: 160, @@ -204,38 +253,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl { id: "actions", header: () => Действия, - cell: ({ row }) => { - const tunnel = row.original - return ( -
- - - - - } - /> - - onExport(tunnel)}> - - Экспорт .rsc - - - -
- ) - }, + cell: VxlanActionsCell, enableSorting: false, size: 72, meta: { @@ -244,7 +262,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl }, }, ], - [onExport, serverMap, failedServerIds], + [serverMap, failedServerIds], ) const table = useTable({ @@ -275,6 +293,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl } return ( + + ) } diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index e1551b89..92b044cd 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-09: backend8eaf78b/frontend479f2ad, backend healthy. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен. +Статус на2026-10-09: backend/frontend ux-vxlan76, backend healthy; раздел VXLAN76 принят в описанных границах. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен. ## Паспорт стенда @@ -647,3 +647,37 @@ Production frontend наCHR1 при1280px/CHR2 при320px направил POST Финальный fixture exit0, own IP→VTEP→interface удалены по receipt IDs с проверкой собственного comment. VXLAN/VTEP/IP/routes/IPv4 filter/nat/mangle/bridge/bridge-port selected projections совпали с baseline на обоих CHR. Динамические ARP/FDB не входили в baseline projection; полнота их очистки этим блоком не заявлена. Backend healthy,3 основные службы, business DB controls2servers/9speedHistory/14retentionDays/9migrations сохранены. [Контрольные данные](ux-ui/acceptance/vxlan-db-controls-2026-10-09.json). Ключей/паролей в JSON нет. Доступность обоих CHR подтверждена native операциями; тестовый серверSSH работает. Product changes/build не выполнялись, локальная общая регрессия не повторялась для read-only UI приёмки. По code прямой /servers/:id/vxlan пока получает server без enabled guard; отключённый сервер здесь может опрашиваться, вопреки ранее принятому правилу пользователя. Следующий12.5.76 — исправить этот endpoint и проверить disabled/no native calls. Реальное отключение устройств в75 не выполнялось. VXLAN create/delete UX,bridge/FDB export/reapply,IPv6/throughput/RBAC/частичные сбои и остальные внешние ограничения открыты. Локальный коммит без push. + +## 12.5.76 — VXLAN: достоверность, экспорт и полная профильная приёмка (2026-10-09) + +По поручению пользователя работа укрупнена по разделам. В одном блоке выполнены code/API/UI, профильные проверки, production Docker build обеих частей, deploy, реальная приёмка и очистка; один локальный коммит в конце, без push. + +Прямой GET /api/servers/:id/vxlan теперь возвращает409 для выключенного сервера до обращения к RouterOS. Защита есть также в native reader и агрегаторе, для home-router и exit-node. Неполные/некорректные параметры интерфейса, повторные ID/имена и неизвестные/некорректные статические VTEP считаются ошибкой устройства вместо выдуманных VNI/port/L2MTU/флагов. Сохранение последних успешных данных и частичная ошибка остаются прежними. VTEP сопоставляется с именем или RouterOS ID интерфейса. + +Реальный MTU передаётся в модель, виден в таблице и экспортируется; для старого снимка без MTU показывается «—», параметр не придумывается, экспорт просит проверить его отдельно. CodeExportSheet прямо описывает fragment: interface/известный MTU/static VTEP, без IP/bridge/FDB. Стабильная actions cell получает callback через контекст, поэтому открытое меню сохраняется при refresh и экспорт использует последние данные. Frame/base-nova и установленные ReUI primitives сохранены; registry migration не выполнялась. + +Проверки: backend mapping/HTTP route/export tests, backend и scoped frontend TypeScript, ESLint изменённых файлов и diff check passed. Профильный browser набор23/23 (VXLAN и два связанных layout cases), context-race4/4. В тесте1000 строк отображаются50 строк; измерено2245 DOM nodes, payload227787 bytes и829ms до ready в данном локальном запуске — это наблюдение, не универсальный performance SLA. Общая регрессия проекта не повторялась. + +На161.104.46.222 развёрнуты backend/frontend mmapp-test/*:ux-vxlan76, version1.0.0-test.vxlan76. Сборочный source — git archive базового HEAD6b068ac с явными13 working-file overrides, а не отдельный уже закоммиченный revision. SHA256 архива b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae; [manifest](ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json) позволяет сверить каждый изменённый файл. Production builds passed; Compose3 services running, backend/PostgreSQL healthy; frontend healthcheck отсутствует, HTTP/browser passed. Backup2441206 bytes/0600, pg_restore --list passed, .env сохранён приватно на сервере, прежние образы доступны для rollback. + +Реальная собственная VXLAN пара на194.226.121.150 и104.165.244.7: VNI160075, UDP51880, MTU1400, публичные local/static remote VTEP, IP198.18.255.245/30↔198.18.255.246/30. Native параметры совпали с каталогом приложения; Ping3/3 в обе стороны. [Browser4/4](ux-ui/acceptance/browser-vxlan-section-2026-10-09.json) подтвердил URL выбранного устройства/туннеля, экспорт и отсутствие page overflow на1280/320px. Экспорт каждой стороны заново применён через строго ограниченный native REST adapter к собственным интерфейсам: только interface/VTEP команды, IP добавлены отдельно; MTU1400 и Ping3/3 повторно подтверждены. Это проверка параметров fragment, не запуск произвольного .rsc скрипта в терминале. + +Две временные выключенные записи home-router/exit-node указывали на отдельный HTTP probe. Прямые GET вернули409, nativeProbeCalls0; временные записи удалены. [Native protocol](ux-ui/acceptance/vxlan-section-native-2026-10-09.json): cleanup сравнил исходные VXLAN/VTEP/IP/routes/IPv4 firewall/bridge/bridge-port на обоих устройствах, все проверки true. DB controls после: servers2/speedHistory9/retention14/migrations9. Пароли не менялись, SSH и оба CHR доступны. [Smoke шести страниц](ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json): HTTP200, mobile fits, JS errors0/API errors0. + +| Референс | Схема применённого решения | +| --- | --- | +| [ReUI Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) / [Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Контекст устройства → достоверная таблица/MTU → устойчивое меню | +| [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Актуальный снимок → границы fragment → код → независимая native проверка | + +```mermaid +flowchart LR + A[Устройство] --> B{Включено?} + B -->|Нет| C[409: без опроса] + B -->|Да| D{Снимок достоверен?} + D -->|Нет| E[Ошибка и последние данные] + D -->|Да| F[Таблица с MTU] + F --> G[Экспорт interface и VTEP] + G --> H[Повторное применение и Ping] +``` + +Границы: app VXLAN API read-only, create/delete выполнены native REST только на собственных объектах. IPv6/bridge/FDB/throughput/RBAC/concurrency и произвольный терминальный .rsc import не приняты; динамический ARP/FDB baseline не сравнивался. Первый запуск Edge в sandbox завершился до страницы; повторный разрешённый запуск прошёл. Этап12.5 целиком не закрыт. Следующий раздел12.5.77 — IPsec; остаточные VXLAN/WireGuard ограничения остаются в протоколе. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 0eaad1bb..af8f8bc1 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,14 +1,16 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-09: 12.5.75 принял собственную реальную VXLAN пару на обоих CHR: native/API параметры и Ping3/3 в обе стороны, browser URL/export4/4 на1280/320px. Fixture создан через RouterOS REST: app VXLAN API только read-only. Cleanup/selected baseline/DB controls passed, runtime backend8eaf78b/frontend479f2ad прежний. Следующий12.5.76 — исключить опрос выключенного сервера в прямом VXLAN endpoint и проверить отсутствие native запросов; затем продолжить профильные ограничения VXLAN. Этап12.5 открыт. +Актуальное продолжение 2026-10-09: 12.5.76 завершил укрупнённый раздел VXLAN: достоверный снимок/MTU, запрет опроса выключенных устройств, устойчивое меню, Docker deploy обеих частей и реальная приёмка двух CHR. Browser4/4, повторное применение экспортированного fragment с MTU1400 и Ping3/3 в обе стороны, probe0 и cleanup passed. Runtime backend/frontend ux-vxlan76, version1.0.0-test.vxlan76; происхождение сборки — base HEAD6b068ac плюс явный manifest13 файлов. Следующий12.5.77 — IPsec целиком: карта API/операций, собственная тестовая пара, профильные проверки и исправления, deployment/приёмка/cleanup, один локальный коммит. Этап12.5 открыт. -Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-08. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. +Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-09. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. -## Актуальная точка продолжения — 2026-10-08 +## Актуальная точка продолжения — 2026-10-09 -Этапы0–11 завершены в описанных локальных границах. Этап12 открыт. На тестовом сервере161.104.46.222 работают backend3722fad/frontendbf43263 и два CHR. Внешние проверки подтвердили PG backup/restore/rollback, остановку фоновой работы для одного экземпляра, API JWT/RBAC и браузерные состояния восстановления. Native TCP/UDP speed, история и UI проверены в границах журнала; доступ ко второму CHR восстановлен пользователем. Очередь Telegram проверена на реальном PG: dedupe, конкурентный claim, retry/version, ошибки сохранения и неопределённый исход; ответы отправителя подменялись, реальной доставки это не доказывает. Блоки12.5.43–45 подтвердили browser retry/checkpoint и развернули понятное пояснение HTTP500. 12.5.46 подтвердил browser JWT/RBAC очереди на реальном HTTP/PG; 12.5.47 подтвердил изоляцию checkpoint при смене двух реальных HTTP/PG источников в конфигурации local; 12.5.48 подтвердил scoped axe/клавиатуру в обеих темах и320px; 12.5.49 собрал Q-матрицу и подтвердил отсутствие настройки Telegram; 12.5.50 подтвердил GET-only инвентаризацию: VPN/BGP/OSPF пусты, роли home-router/home-router; По уточнению пользователя12.5.51 создал реальные GRE туннели и подтвердил lifecycle/связность; обнаружена потеря peer IP, 12.5.52 локально исправил сохранение peer metadata, профильные проверки passed; 12.5.53 развернул исправление и подтвердил его на реальной GRE паре/API/UI; 12.5.54 локально исправил устойчивость меню GRE, профильные проверки passed; 12.5.55 развернул frontend731803b и подтвердил устойчивость меню на реальной GRE паре; 12.5.56 локально адаптировал внутренние IP поля GRE для320px, обе темы и профильные проверки passed; 12.5.57 развернул frontendfd521f6 и подтвердил мобильную GRE форму на реальной паре CHR; 12.5.58 подтвердил настоящую WireGuard пару, handshake/Ping/native/UI и финальную очистку; 12.5.59 подтвердил оставшиеся peers/IP после удаления WG и локально исправил backend; 12.5.60 развернул backend/frontend3722fad и подтвердил полное обычное удаление WG; 12.5.61 подтвердил UI создание WG и локально исправил remount меню; переключение осталось открытым. 12.5.62 развернул frontend6a46fb1 и подтвердил полный WG UI lifecycle; 12.5.63 локально адаптировал ширину трёх WireGuard Sheets и подтвердил footer/клавиатуру в обеих темах; 12.5.64 развернул frontendbf43263 и подтвердил live mobile Sheets/UI lifecycle; 12.5.65 подтвердил API export false redaction/RSC-CONF параметры и нашёл ошибочный client endpoint вpeer-conf; 12.5.66 локально исправил client endpoint и явные template placeholders; следующий12.5.67 — Docker deploy обеих частей/contracts и повторный реальный Peer .conf export/preview; import после этого. Реальная Telegram доставка, Portal SSO, физический TCP disconnect, multi-instance и остальные профильные сетевые сценарии остаются открытыми. [Протокол](ux-ui-external-acceptance.md) и [журнал](ux-ui-stage-12.md) содержат доказательства и ограничения. +Актуальное продолжение 2026-10-09: 12.5.76 завершил укрупнённый раздел VXLAN: достоверный снимок/MTU, запрет опроса выключенных устройств, устойчивое меню, Docker deploy обеих частей и реальная приёмка двух CHR. Browser4/4, повторное применение экспортированного fragment с MTU1400 и Ping3/3 в обе стороны, probe0 и cleanup passed. Runtime backend/frontend ux-vxlan76, version1.0.0-test.vxlan76; происхождение сборки — base HEAD6b068ac плюс явный manifest13 файлов. Следующий12.5.77 — IPsec целиком: карта API/операций, собственная тестовая пара, профильные проверки и исправления, deployment/приёмка/cleanup, один локальный коммит. Этап12.5 открыт. -Выполненные внешние сценарии не заменяют приёмку оставшихся интеграций и условий эксплуатации. Датированные продолжения ниже — история; точный следующий шаг указан в начале [журнала](ux-ui-progress.md). +Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. + +По поручению2026-10-09 работать укрупнёнными разделами, последовательно на текущей модели: карта возможностей → исправления → профильные тесты → build/deploy → собственные реальные сценарии → cleanup → один локальный коммит без push. Проверять квоту после завершённого раздела; незавершённые проверки явно переносить в журнал. ## Цель и обязательные требования @@ -332,3 +334,37 @@ REST запрос → ожидание → подтверждённый output / Обнаружен настоящий дефект: принятый 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.76 — VXLAN: достоверность, экспорт и полная профильная приёмка (2026-10-09) + +По поручению пользователя работа укрупнена по разделам. В одном блоке выполнены code/API/UI, профильные проверки, production Docker build обеих частей, deploy, реальная приёмка и очистка; один локальный коммит в конце, без push. + +Прямой GET /api/servers/:id/vxlan теперь возвращает409 для выключенного сервера до обращения к RouterOS. Защита есть также в native reader и агрегаторе, для home-router и exit-node. Неполные/некорректные параметры интерфейса, повторные ID/имена и неизвестные/некорректные статические VTEP считаются ошибкой устройства вместо выдуманных VNI/port/L2MTU/флагов. Сохранение последних успешных данных и частичная ошибка остаются прежними. VTEP сопоставляется с именем или RouterOS ID интерфейса. + +Реальный MTU передаётся в модель, виден в таблице и экспортируется; для старого снимка без MTU показывается «—», параметр не придумывается, экспорт просит проверить его отдельно. CodeExportSheet прямо описывает fragment: interface/известный MTU/static VTEP, без IP/bridge/FDB. Стабильная actions cell получает callback через контекст, поэтому открытое меню сохраняется при refresh и экспорт использует последние данные. Frame/base-nova и установленные ReUI primitives сохранены; registry migration не выполнялась. + +Проверки: backend mapping/HTTP route/export tests, backend и scoped frontend TypeScript, ESLint изменённых файлов и diff check passed. Профильный browser набор23/23 (VXLAN и два связанных layout cases), context-race4/4. В тесте1000 строк отображаются50 строк; измерено2245 DOM nodes, payload227787 bytes и829ms до ready в данном локальном запуске — это наблюдение, не универсальный performance SLA. Общая регрессия проекта не повторялась. + +На161.104.46.222 развёрнуты backend/frontend mmapp-test/*:ux-vxlan76, version1.0.0-test.vxlan76. Сборочный source — git archive базового HEAD6b068ac с явными13 working-file overrides, а не отдельный уже закоммиченный revision. SHA256 архива b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae; [manifest](ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json) позволяет сверить каждый изменённый файл. Production builds passed; Compose3 services running, backend/PostgreSQL healthy; frontend healthcheck отсутствует, HTTP/browser passed. Backup2441206 bytes/0600, pg_restore --list passed, .env сохранён приватно на сервере, прежние образы доступны для rollback. + +Реальная собственная VXLAN пара на194.226.121.150 и104.165.244.7: VNI160075, UDP51880, MTU1400, публичные local/static remote VTEP, IP198.18.255.245/30↔198.18.255.246/30. Native параметры совпали с каталогом приложения; Ping3/3 в обе стороны. [Browser4/4](ux-ui/acceptance/browser-vxlan-section-2026-10-09.json) подтвердил URL выбранного устройства/туннеля, экспорт и отсутствие page overflow на1280/320px. Экспорт каждой стороны заново применён через строго ограниченный native REST adapter к собственным интерфейсам: только interface/VTEP команды, IP добавлены отдельно; MTU1400 и Ping3/3 повторно подтверждены. Это проверка параметров fragment, не запуск произвольного .rsc скрипта в терминале. + +Две временные выключенные записи home-router/exit-node указывали на отдельный HTTP probe. Прямые GET вернули409, nativeProbeCalls0; временные записи удалены. [Native protocol](ux-ui/acceptance/vxlan-section-native-2026-10-09.json): cleanup сравнил исходные VXLAN/VTEP/IP/routes/IPv4 firewall/bridge/bridge-port на обоих устройствах, все проверки true. DB controls после: servers2/speedHistory9/retention14/migrations9. Пароли не менялись, SSH и оба CHR доступны. [Smoke шести страниц](ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json): HTTP200, mobile fits, JS errors0/API errors0. + +| Референс | Схема применённого решения | +| --- | --- | +| [ReUI Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) / [Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Контекст устройства → достоверная таблица/MTU → устойчивое меню | +| [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Актуальный снимок → границы fragment → код → независимая native проверка | + +```mermaid +flowchart LR + A[Устройство] --> B{Включено?} + B -->|Нет| C[409: без опроса] + B -->|Да| D{Снимок достоверен?} + D -->|Нет| E[Ошибка и последние данные] + D -->|Да| F[Таблица с MTU] + F --> G[Экспорт interface и VTEP] + G --> H[Повторное применение и Ping] +``` + +Границы: app VXLAN API read-only, create/delete выполнены native REST только на собственных объектах. IPv6/bridge/FDB/throughput/RBAC/concurrency и произвольный терминальный .rsc import не приняты; динамический ARP/FDB baseline не сравнивался. Первый запуск Edge в sandbox завершился до страницы; повторный разрешённый запуск прошёл. Этап12.5 целиком не закрыт. Следующий раздел12.5.77 — IPsec; остаточные VXLAN/WireGuard ограничения остаются в протоколе. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ebbbf198..370433fd 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,11 +1,13 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-09: 12.5.75 принял собственную реальную VXLAN пару на обоих CHR: native/API параметры и Ping3/3 в обе стороны, browser URL/export4/4 на1280/320px. Fixture создан через RouterOS REST: app VXLAN API только read-only. Cleanup/selected baseline/DB controls passed, runtime backend8eaf78b/frontend479f2ad прежний. Следующий12.5.76 — исключить опрос выключенного сервера в прямом VXLAN endpoint и проверить отсутствие native запросов; затем продолжить профильные ограничения VXLAN. Этап12.5 открыт. +Актуальное продолжение 2026-10-09: 12.5.76 завершил укрупнённый раздел VXLAN: достоверный снимок/MTU, запрет опроса выключенных устройств, устойчивое меню, Docker deploy обеих частей и реальная приёмка двух CHR. Browser4/4, повторное применение экспортированного fragment с MTU1400 и Ping3/3 в обе стороны, probe0 и cleanup passed. Runtime backend/frontend ux-vxlan76, version1.0.0-test.vxlan76; происхождение сборки — base HEAD6b068ac плюс явный manifest13 файлов. Следующий12.5.77 — IPsec целиком: карта API/операций, собственная тестовая пара, профильные проверки и исправления, deployment/приёмка/cleanup, один локальный коммит. Этап12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). ## Правила продолжения +- По поручению2026-10-09 укрупнять работу по разделам: исправления, профильные проверки, build/deploy и реальная приёмка входят в один завершённый блок; один локальный коммит после него. + - По поручению2026-10-08 при внешнем тестировании VPN создавать реальные собственные тестовые туннели, особенно GRE; проверять API/RouterOS/связность и очищать только созданные объекты, сохраняя исходную конфигурацию. - План: ReUI PRO, Frame, base-nova; KpiStatGrid, ServerTileRail и CodeExportSheet остаются источниками общих композиционных решений. @@ -34,7 +36,41 @@ | 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–75 и ограничения ниже. Runtime backend8eaf78b/frontend479f2ad. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–76 и ограничения ниже. Runtime backend/frontend ux-vxlan76. | + +## 12.5.76 — VXLAN: достоверность, экспорт и полная профильная приёмка (2026-10-09) + +По поручению пользователя работа укрупнена по разделам. В одном блоке выполнены code/API/UI, профильные проверки, production Docker build обеих частей, deploy, реальная приёмка и очистка; один локальный коммит в конце, без push. + +Прямой GET /api/servers/:id/vxlan теперь возвращает409 для выключенного сервера до обращения к RouterOS. Защита есть также в native reader и агрегаторе, для home-router и exit-node. Неполные/некорректные параметры интерфейса, повторные ID/имена и неизвестные/некорректные статические VTEP считаются ошибкой устройства вместо выдуманных VNI/port/L2MTU/флагов. Сохранение последних успешных данных и частичная ошибка остаются прежними. VTEP сопоставляется с именем или RouterOS ID интерфейса. + +Реальный MTU передаётся в модель, виден в таблице и экспортируется; для старого снимка без MTU показывается «—», параметр не придумывается, экспорт просит проверить его отдельно. CodeExportSheet прямо описывает fragment: interface/известный MTU/static VTEP, без IP/bridge/FDB. Стабильная actions cell получает callback через контекст, поэтому открытое меню сохраняется при refresh и экспорт использует последние данные. Frame/base-nova и установленные ReUI primitives сохранены; registry migration не выполнялась. + +Проверки: backend mapping/HTTP route/export tests, backend и scoped frontend TypeScript, ESLint изменённых файлов и diff check passed. Профильный browser набор23/23 (VXLAN и два связанных layout cases), context-race4/4. В тесте1000 строк отображаются50 строк; измерено2245 DOM nodes, payload227787 bytes и829ms до ready в данном локальном запуске — это наблюдение, не универсальный performance SLA. Общая регрессия проекта не повторялась. + +На161.104.46.222 развёрнуты backend/frontend mmapp-test/*:ux-vxlan76, version1.0.0-test.vxlan76. Сборочный source — git archive базового HEAD6b068ac с явными13 working-file overrides, а не отдельный уже закоммиченный revision. SHA256 архива b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae; [manifest](ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json) позволяет сверить каждый изменённый файл. Production builds passed; Compose3 services running, backend/PostgreSQL healthy; frontend healthcheck отсутствует, HTTP/browser passed. Backup2441206 bytes/0600, pg_restore --list passed, .env сохранён приватно на сервере, прежние образы доступны для rollback. + +Реальная собственная VXLAN пара на194.226.121.150 и104.165.244.7: VNI160075, UDP51880, MTU1400, публичные local/static remote VTEP, IP198.18.255.245/30↔198.18.255.246/30. Native параметры совпали с каталогом приложения; Ping3/3 в обе стороны. [Browser4/4](ux-ui/acceptance/browser-vxlan-section-2026-10-09.json) подтвердил URL выбранного устройства/туннеля, экспорт и отсутствие page overflow на1280/320px. Экспорт каждой стороны заново применён через строго ограниченный native REST adapter к собственным интерфейсам: только interface/VTEP команды, IP добавлены отдельно; MTU1400 и Ping3/3 повторно подтверждены. Это проверка параметров fragment, не запуск произвольного .rsc скрипта в терминале. + +Две временные выключенные записи home-router/exit-node указывали на отдельный HTTP probe. Прямые GET вернули409, nativeProbeCalls0; временные записи удалены. [Native protocol](ux-ui/acceptance/vxlan-section-native-2026-10-09.json): cleanup сравнил исходные VXLAN/VTEP/IP/routes/IPv4 firewall/bridge/bridge-port на обоих устройствах, все проверки true. DB controls после: servers2/speedHistory9/retention14/migrations9. Пароли не менялись, SSH и оба CHR доступны. [Smoke шести страниц](ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json): HTTP200, mobile fits, JS errors0/API errors0. + +| Референс | Схема применённого решения | +| --- | --- | +| [ReUI Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) / [Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Контекст устройства → достоверная таблица/MTU → устойчивое меню | +| [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Актуальный снимок → границы fragment → код → независимая native проверка | + +```mermaid +flowchart LR + A[Устройство] --> B{Включено?} + B -->|Нет| C[409: без опроса] + B -->|Да| D{Снимок достоверен?} + D -->|Нет| E[Ошибка и последние данные] + D -->|Да| F[Таблица с MTU] + F --> G[Экспорт interface и VTEP] + G --> H[Повторное применение и Ping] +``` + +Границы: app VXLAN API read-only, create/delete выполнены native REST только на собственных объектах. IPv6/bridge/FDB/throughput/RBAC/concurrency и произвольный терминальный .rsc import не приняты; динамический ARP/FDB baseline не сравнивался. Первый запуск Edge в sandbox завершился до страницы; повторный разрешённый запуск прошёл. Этап12.5 целиком не закрыт. Следующий раздел12.5.77 — IPsec; остаточные VXLAN/WireGuard ограничения остаются в протоколе. ## 12.5.75 — собственная VXLAN пара и реальный каталог/экспорт (2026-10-09) diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index dffd3681..171295d7 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-09: 12.5.75 принял собственную реальную VXLAN пару на обоих CHR: native/API параметры и Ping3/3 в обе стороны, browser URL/export4/4 на1280/320px. Fixture создан через RouterOS REST: app VXLAN API только read-only. Cleanup/selected baseline/DB controls passed, runtime backend8eaf78b/frontend479f2ad прежний. Следующий12.5.76 — исключить опрос выключенного сервера в прямом VXLAN endpoint и проверить отсутствие native запросов; затем продолжить профильные ограничения VXLAN. Этап12.5 открыт. +Актуальное продолжение 2026-10-09: 12.5.76 завершил укрупнённый раздел VXLAN: достоверный снимок/MTU, запрет опроса выключенных устройств, устойчивое меню, Docker deploy обеих частей и реальная приёмка двух CHR. Browser4/4, повторное применение экспортированного fragment с MTU1400 и Ping3/3 в обе стороны, probe0 и cleanup passed. Runtime backend/frontend ux-vxlan76, version1.0.0-test.vxlan76; происхождение сборки — base HEAD6b068ac плюс явный manifest13 файлов. Следующий12.5.77 — IPsec целиком: карта API/операций, собственная тестовая пара, профильные проверки и исправления, deployment/приёмка/cleanup, один локальный коммит. Этап12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -1812,3 +1812,37 @@ Production frontend наCHR1 при1280px/CHR2 при320px направил POST Финальный fixture exit0, own IP→VTEP→interface удалены по receipt IDs с проверкой собственного comment. VXLAN/VTEP/IP/routes/IPv4 filter/nat/mangle/bridge/bridge-port selected projections совпали с baseline на обоих CHR. Динамические ARP/FDB не входили в baseline projection; полнота их очистки этим блоком не заявлена. Backend healthy,3 основные службы, business DB controls2servers/9speedHistory/14retentionDays/9migrations сохранены. [Контрольные данные](ux-ui/acceptance/vxlan-db-controls-2026-10-09.json). Ключей/паролей в JSON нет. Доступность обоих CHR подтверждена native операциями; тестовый серверSSH работает. Product changes/build не выполнялись, локальная общая регрессия не повторялась для read-only UI приёмки. По code прямой /servers/:id/vxlan пока получает server без enabled guard; отключённый сервер здесь может опрашиваться, вопреки ранее принятому правилу пользователя. Следующий12.5.76 — исправить этот endpoint и проверить disabled/no native calls. Реальное отключение устройств в75 не выполнялось. VXLAN create/delete UX,bridge/FDB export/reapply,IPv6/throughput/RBAC/частичные сбои и остальные внешние ограничения открыты. Локальный коммит без push. + +## 12.5.76 — VXLAN: достоверность, экспорт и полная профильная приёмка (2026-10-09) + +По поручению пользователя работа укрупнена по разделам. В одном блоке выполнены code/API/UI, профильные проверки, production Docker build обеих частей, deploy, реальная приёмка и очистка; один локальный коммит в конце, без push. + +Прямой GET /api/servers/:id/vxlan теперь возвращает409 для выключенного сервера до обращения к RouterOS. Защита есть также в native reader и агрегаторе, для home-router и exit-node. Неполные/некорректные параметры интерфейса, повторные ID/имена и неизвестные/некорректные статические VTEP считаются ошибкой устройства вместо выдуманных VNI/port/L2MTU/флагов. Сохранение последних успешных данных и частичная ошибка остаются прежними. VTEP сопоставляется с именем или RouterOS ID интерфейса. + +Реальный MTU передаётся в модель, виден в таблице и экспортируется; для старого снимка без MTU показывается «—», параметр не придумывается, экспорт просит проверить его отдельно. CodeExportSheet прямо описывает fragment: interface/известный MTU/static VTEP, без IP/bridge/FDB. Стабильная actions cell получает callback через контекст, поэтому открытое меню сохраняется при refresh и экспорт использует последние данные. Frame/base-nova и установленные ReUI primitives сохранены; registry migration не выполнялась. + +Проверки: backend mapping/HTTP route/export tests, backend и scoped frontend TypeScript, ESLint изменённых файлов и diff check passed. Профильный browser набор23/23 (VXLAN и два связанных layout cases), context-race4/4. В тесте1000 строк отображаются50 строк; измерено2245 DOM nodes, payload227787 bytes и829ms до ready в данном локальном запуске — это наблюдение, не универсальный performance SLA. Общая регрессия проекта не повторялась. + +На161.104.46.222 развёрнуты backend/frontend mmapp-test/*:ux-vxlan76, version1.0.0-test.vxlan76. Сборочный source — git archive базового HEAD6b068ac с явными13 working-file overrides, а не отдельный уже закоммиченный revision. SHA256 архива b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae; [manifest](ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json) позволяет сверить каждый изменённый файл. Production builds passed; Compose3 services running, backend/PostgreSQL healthy; frontend healthcheck отсутствует, HTTP/browser passed. Backup2441206 bytes/0600, pg_restore --list passed, .env сохранён приватно на сервере, прежние образы доступны для rollback. + +Реальная собственная VXLAN пара на194.226.121.150 и104.165.244.7: VNI160075, UDP51880, MTU1400, публичные local/static remote VTEP, IP198.18.255.245/30↔198.18.255.246/30. Native параметры совпали с каталогом приложения; Ping3/3 в обе стороны. [Browser4/4](ux-ui/acceptance/browser-vxlan-section-2026-10-09.json) подтвердил URL выбранного устройства/туннеля, экспорт и отсутствие page overflow на1280/320px. Экспорт каждой стороны заново применён через строго ограниченный native REST adapter к собственным интерфейсам: только interface/VTEP команды, IP добавлены отдельно; MTU1400 и Ping3/3 повторно подтверждены. Это проверка параметров fragment, не запуск произвольного .rsc скрипта в терминале. + +Две временные выключенные записи home-router/exit-node указывали на отдельный HTTP probe. Прямые GET вернули409, nativeProbeCalls0; временные записи удалены. [Native protocol](ux-ui/acceptance/vxlan-section-native-2026-10-09.json): cleanup сравнил исходные VXLAN/VTEP/IP/routes/IPv4 firewall/bridge/bridge-port на обоих устройствах, все проверки true. DB controls после: servers2/speedHistory9/retention14/migrations9. Пароли не менялись, SSH и оба CHR доступны. [Smoke шести страниц](ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json): HTTP200, mobile fits, JS errors0/API errors0. + +| Референс | Схема применённого решения | +| --- | --- | +| [ReUI Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) / [Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Контекст устройства → достоверная таблица/MTU → устойчивое меню | +| [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Актуальный снимок → границы fragment → код → независимая native проверка | + +```mermaid +flowchart LR + A[Устройство] --> B{Включено?} + B -->|Нет| C[409: без опроса] + B -->|Да| D{Снимок достоверен?} + D -->|Нет| E[Ошибка и последние данные] + D -->|Да| F[Таблица с MTU] + F --> G[Экспорт interface и VTEP] + G --> H[Повторное применение и Ping] +``` + +Границы: app VXLAN API read-only, create/delete выполнены native REST только на собственных объектах. IPv6/bridge/FDB/throughput/RBAC/concurrency и произвольный терминальный .rsc import не приняты; динамический ARP/FDB baseline не сравнивался. Первый запуск Edge в sandbox завершился до страницы; повторный разрешённый запуск прошёл. Этап12.5 целиком не закрыт. Следующий раздел12.5.77 — IPsec; остаточные VXLAN/WireGuard ограничения остаются в протоколе. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 50607e2d..de9cf37c 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-09 -Сервер161.104.46.222 предоставлен для временного тестирования. Backend source8eaf78b68631d774bf18ae049d0e11f90fe098d8/version1.0.0-test.8eaf78b healthy,frontend479f2ad/version1.0.0-test.479f2ad сохранён. SSH/оба CHR доступны с прежними доступами. Real rollback/retry72 и изолированный PG audit отказ после native apply74 приняты в описанных границах; роли/fixture/process удалены, baseline восстановлен. VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен. +Сервер161.104.46.222 предоставлен для временного тестирования. Backend/frontend ux-vxlan76, version1.0.0-test.vxlan76; backend/PostgreSQL healthy. SSH и оба CHR доступны с прежними доступами. Раздел VXLAN76 принят в описанных ниже границах; собственные объекты и временные выключенные DB записи удалены, selected baseline восстановлен. | Параметр | Проверенное состояние | | --- | --- | @@ -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-8eaf78b и mmapp-test/frontend:ux-479f2ad; запущены | +| Образы приложения | mmapp-test/backend:ux-vxlan76 и mmapp-test/frontend:ux-vxlan76; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -655,3 +655,37 @@ Production frontend наCHR1 при1280px/CHR2 при320px направил POST Финальный fixture exit0, own IP→VTEP→interface удалены по receipt IDs с проверкой собственного comment. VXLAN/VTEP/IP/routes/IPv4 filter/nat/mangle/bridge/bridge-port selected projections совпали с baseline на обоих CHR. Динамические ARP/FDB не входили в baseline projection; полнота их очистки этим блоком не заявлена. Backend healthy,3 основные службы, business DB controls2servers/9speedHistory/14retentionDays/9migrations сохранены. [Контрольные данные](ux-ui/acceptance/vxlan-db-controls-2026-10-09.json). Ключей/паролей в JSON нет. Доступность обоих CHR подтверждена native операциями; тестовый серверSSH работает. Product changes/build не выполнялись, локальная общая регрессия не повторялась для read-only UI приёмки. По code прямой /servers/:id/vxlan пока получает server без enabled guard; отключённый сервер здесь может опрашиваться, вопреки ранее принятому правилу пользователя. Следующий12.5.76 — исправить этот endpoint и проверить disabled/no native calls. Реальное отключение устройств в75 не выполнялось. VXLAN create/delete UX,bridge/FDB export/reapply,IPv6/throughput/RBAC/частичные сбои и остальные внешние ограничения открыты. Локальный коммит без push. + +## 12.5.76 — VXLAN: достоверность, экспорт и полная профильная приёмка (2026-10-09) + +По поручению пользователя работа укрупнена по разделам. В одном блоке выполнены code/API/UI, профильные проверки, production Docker build обеих частей, deploy, реальная приёмка и очистка; один локальный коммит в конце, без push. + +Прямой GET /api/servers/:id/vxlan теперь возвращает409 для выключенного сервера до обращения к RouterOS. Защита есть также в native reader и агрегаторе, для home-router и exit-node. Неполные/некорректные параметры интерфейса, повторные ID/имена и неизвестные/некорректные статические VTEP считаются ошибкой устройства вместо выдуманных VNI/port/L2MTU/флагов. Сохранение последних успешных данных и частичная ошибка остаются прежними. VTEP сопоставляется с именем или RouterOS ID интерфейса. + +Реальный MTU передаётся в модель, виден в таблице и экспортируется; для старого снимка без MTU показывается «—», параметр не придумывается, экспорт просит проверить его отдельно. CodeExportSheet прямо описывает fragment: interface/известный MTU/static VTEP, без IP/bridge/FDB. Стабильная actions cell получает callback через контекст, поэтому открытое меню сохраняется при refresh и экспорт использует последние данные. Frame/base-nova и установленные ReUI primitives сохранены; registry migration не выполнялась. + +Проверки: backend mapping/HTTP route/export tests, backend и scoped frontend TypeScript, ESLint изменённых файлов и diff check passed. Профильный browser набор23/23 (VXLAN и два связанных layout cases), context-race4/4. В тесте1000 строк отображаются50 строк; измерено2245 DOM nodes, payload227787 bytes и829ms до ready в данном локальном запуске — это наблюдение, не универсальный performance SLA. Общая регрессия проекта не повторялась. + +На161.104.46.222 развёрнуты backend/frontend mmapp-test/*:ux-vxlan76, version1.0.0-test.vxlan76. Сборочный source — git archive базового HEAD6b068ac с явными13 working-file overrides, а не отдельный уже закоммиченный revision. SHA256 архива b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae; [manifest](ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json) позволяет сверить каждый изменённый файл. Production builds passed; Compose3 services running, backend/PostgreSQL healthy; frontend healthcheck отсутствует, HTTP/browser passed. Backup2441206 bytes/0600, pg_restore --list passed, .env сохранён приватно на сервере, прежние образы доступны для rollback. + +Реальная собственная VXLAN пара на194.226.121.150 и104.165.244.7: VNI160075, UDP51880, MTU1400, публичные local/static remote VTEP, IP198.18.255.245/30↔198.18.255.246/30. Native параметры совпали с каталогом приложения; Ping3/3 в обе стороны. [Browser4/4](ux-ui/acceptance/browser-vxlan-section-2026-10-09.json) подтвердил URL выбранного устройства/туннеля, экспорт и отсутствие page overflow на1280/320px. Экспорт каждой стороны заново применён через строго ограниченный native REST adapter к собственным интерфейсам: только interface/VTEP команды, IP добавлены отдельно; MTU1400 и Ping3/3 повторно подтверждены. Это проверка параметров fragment, не запуск произвольного .rsc скрипта в терминале. + +Две временные выключенные записи home-router/exit-node указывали на отдельный HTTP probe. Прямые GET вернули409, nativeProbeCalls0; временные записи удалены. [Native protocol](ux-ui/acceptance/vxlan-section-native-2026-10-09.json): cleanup сравнил исходные VXLAN/VTEP/IP/routes/IPv4 firewall/bridge/bridge-port на обоих устройствах, все проверки true. DB controls после: servers2/speedHistory9/retention14/migrations9. Пароли не менялись, SSH и оба CHR доступны. [Smoke шести страниц](ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json): HTTP200, mobile fits, JS errors0/API errors0. + +| Референс | Схема применённого решения | +| --- | --- | +| [ReUI Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp) / [Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp) | Контекст устройства → достоверная таблица/MTU → устойчивое меню | +| [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Актуальный снимок → границы fragment → код → независимая native проверка | + +```mermaid +flowchart LR + A[Устройство] --> B{Включено?} + B -->|Нет| C[409: без опроса] + B -->|Да| D{Снимок достоверен?} + D -->|Нет| E[Ошибка и последние данные] + D -->|Да| F[Таблица с MTU] + F --> G[Экспорт interface и VTEP] + G --> H[Повторное применение и Ping] +``` + +Границы: app VXLAN API read-only, create/delete выполнены native REST только на собственных объектах. IPv6/bridge/FDB/throughput/RBAC/concurrency и произвольный терминальный .rsc import не приняты; динамический ARP/FDB baseline не сравнивался. Первый запуск Edge в sandbox завершился до страницы; повторный разрешённый запуск прошёл. Этап12.5 целиком не закрыт. Следующий раздел12.5.77 — IPsec; остаточные VXLAN/WireGuard ограничения остаются в протоколе. diff --git a/docs/ux-ui/acceptance/browser-vxlan-section-2026-10-09.json b/docs/ux-ui/acceptance/browser-vxlan-section-2026-10-09.json new file mode 100644 index 00000000..1884dd08 --- /dev/null +++ b/docs/ux-ui/acceptance/browser-vxlan-section-2026-10-09.json @@ -0,0 +1,57 @@ +{ + "date": "2026-10-09", + "backendSource": "vxlan76", + "frontendSource": "vxlan76", + "name": "mm-acceptance-vx-1791516860664", + "httpMocks": false, + "cases": [ + { + "serverId": 1, + "width": 1280, + "selectedTunnelConfirmed": true, + "exportParametersMatch": true, + "noPageOverflow": true + }, + { + "serverId": 1, + "width": 320, + "selectedTunnelConfirmed": true, + "exportParametersMatch": true, + "noPageOverflow": true + }, + { + "serverId": 2, + "width": 1280, + "selectedTunnelConfirmed": true, + "exportParametersMatch": true, + "noPageOverflow": true + }, + { + "serverId": 2, + "width": 320, + "selectedTunnelConfirmed": true, + "exportParametersMatch": true, + "noPageOverflow": true + } + ], + "replay": { + "approvedInterfaceAndVtepCommandsOnly": true, + "actualMtuPreserved": true, + "ipAddedSeparately": true, + "pings": [ + { + "serverId": 1, + "received": 3 + }, + { + "serverId": 2, + "received": 3 + } + ] + }, + "pageErrors": [], + "limits": [ + "Read-only inventory/export; no app mutation API", + "Fragment reapplied to own native interfaces; MTU preserved, IP added separately, bridge/FDB excluded" + ] +} diff --git a/docs/ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json b/docs/ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json new file mode 100644 index 00000000..cc74cc5a --- /dev/null +++ b/docs/ux-ui/acceptance/deploy-browser-vxlan-section-2026-10-09.json @@ -0,0 +1,40 @@ +{ + "date": "2026-10-09", + "backendSource": "vxlan76", + "frontendSource": "vxlan76", + "httpMocks": false, + "pages": [ + { + "path": "/dashboard", + "status": 200, + "mobileFits": true + }, + { + "path": "/servers", + "status": 200, + "mobileFits": true + }, + { + "path": "/uptime?tab=speed", + "status": 200, + "mobileFits": true + }, + { + "path": "/settings", + "status": 200, + "mobileFits": true + }, + { + "path": "/filters", + "status": 200, + "mobileFits": true + }, + { + "path": "/wireguard", + "status": 200, + "mobileFits": true + } + ], + "pageErrors": [], + "badApiResponses": [] +} diff --git a/docs/ux-ui/acceptance/deploy-vxlan-section-2026-10-09.json b/docs/ux-ui/acceptance/deploy-vxlan-section-2026-10-09.json new file mode 100644 index 00000000..b2a18a02 --- /dev/null +++ b/docs/ux-ui/acceptance/deploy-vxlan-section-2026-10-09.json @@ -0,0 +1,37 @@ +{ + "date": "2026-10-09", + "source": { + "baseHead": "6b068ac", + "workingFileOverrides": 13, + "archiveSha256": "b69f8eb8b5830e85930aca0974cd83a94940e2e162101b8798208720ccf3e1ae", + "imageRevisionLabel": "vxlan76", + "note": "Archive of base commit with explicit uncommitted product overrides; final local commit follows acceptance" + }, + "images": { + "backend": "sha256:5963d0cc7e25ffc3591b5306c85b9df19edf4418bec9a50bf79b0e67519072994", + "frontend": "sha256:1e906c553b5f5af173ff516e132ef1cdefc22a4d73d3662cc200c1265451733b" + }, + "version": "1.0.0-test.vxlan76", + "productionBuildsPassed": true, + "services": { + "running": 3, + "backend": "healthy", + "postgres": "healthy", + "frontend": "no healthcheck; HTTP/browser passed" + }, + "backup": { + "bytes": 2441206, + "mode": "0600", + "pgRestoreListPassed": true, + "privateEnvPreserved": true + }, + "databaseAfter": { + "servers": 2, + "speedHistory": 9, + "retentionDays": 14, + "migrations": 9 + }, + "disabledTemporaryRecordsRemoved": true, + "nativeProbeCalls": 0, + "previousImagesRetained": true +} diff --git a/docs/ux-ui/acceptance/vxlan-section-native-2026-10-09.json b/docs/ux-ui/acceptance/vxlan-section-native-2026-10-09.json new file mode 100644 index 00000000..5092f92f --- /dev/null +++ b/docs/ux-ui/acceptance/vxlan-section-native-2026-10-09.json @@ -0,0 +1,97 @@ +{ + "date": "2026-10-09", + "backendSource": "vxlan76", + "frontendSource": "vxlan76", + "name": "mm-acceptance-vx-1791516860664", + "vni": 160075, + "port": 51880, + "realRouterOs": true, + "creation": "Native REST; app VXLAN API is read-only", + "cleanup": [ + { + "serverId": 1, + "/interface/vxlan": true, + "/interface/vxlan/vteps": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ip/firewall/mangle": true, + "/interface/bridge": true, + "/interface/bridge/port": true + }, + { + "serverId": 2, + "/interface/vxlan": true, + "/interface/vxlan/vteps": true, + "/ip/address": true, + "/ip/route": true, + "/ip/firewall/filter": true, + "/ip/firewall/nat": true, + "/ip/firewall/mangle": true, + "/interface/bridge": true, + "/interface/bridge/port": true + } + ], + "disabledChecks": [ + { + "type": "home-router", + "status": 409, + "noNativeRequests": true + }, + { + "type": "exit-node", + "status": 409, + "noNativeRequests": true + } + ], + "pings": [ + { + "serverId": 1, + "target": "198.18.255.246", + "received": 3 + }, + { + "serverId": 2, + "target": "198.18.255.245", + "received": 3 + } + ], + "catalog": [ + { + "serverId": 1, + "id": "1-*23", + "parametersMatch": true, + "l2mtu": 65535, + "status": "up" + }, + { + "serverId": 2, + "id": "2-*1E", + "parametersMatch": true, + "l2mtu": 65535, + "status": "up" + } + ], + "exportReplay": { + "approvedInterfaceAndVtepCommandsOnly": true, + "actualMtuPreserved": true, + "ipAddedSeparately": true, + "pings": [ + { + "serverId": 1, + "received": 3 + }, + { + "serverId": 2, + "received": 3 + } + ] + }, + "disabledRecordCleanup": true, + "nativeProbeCalls": 0, + "limits": [ + "No app create/delete API; fixture created and removed via native REST", + "Export fragment replayed via whitelisted native REST; IP supplied separately; no bridge/FDB/throughput/IPv6/RBAC" + ] +} diff --git a/docs/ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json b/docs/ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json new file mode 100644 index 00000000..76177bd7 --- /dev/null +++ b/docs/ux-ui/acceptance/vxlan-section-source-manifest-2026-10-09.json @@ -0,0 +1,54 @@ +[ + { + "path": "backend/package.json", + "sha256": "259175f899c51e2cd026269612ad9972c622c009b498b68e66b195c3a7c33979" + }, + { + "path": "backend/src/routes/vxlan.ts", + "sha256": "0bdec6ed828e4fe4e59907370ff8f18d6049c5029d0bb365547074a82ce02904" + }, + { + "path": "backend/src/routes/vxlan.test.ts", + "sha256": "69270487e7e8a907bf1183d8bfa38c4e345fb8c138bed270774d59694aace8f4" + }, + { + "path": "backend/src/services/vxlan-live.ts", + "sha256": "1be3d6f71074b785b80b83190e5e2c1740d966f6387d0261a7dfc20075eed63e" + }, + { + "path": "backend/src/services/vxlan-live.test.ts", + "sha256": "c3b8a145291d564eb153cb3eb144f513d985d8effaaff06ad034c1f6ca399e10" + }, + { + "path": "app/(main)/vxlan/page.tsx", + "sha256": "b3fd63ea61c773509403b9ff772f96045f370c1867aced96d14909d649f96774" + }, + { + "path": "components/data-grids/vxlan-data-grid.tsx", + "sha256": "248050290e7eef3f5b2eb62c873414fde7c7fd1485ef33bdb265a766fcebd74d" + }, + { + "path": "lib/data.ts", + "sha256": "8591c9c6bb62abe7cd47058295c48c194f818cb73bd1b1df9365204492b6810d" + }, + { + "path": "lib/vxlan-response.ts", + "sha256": "1b51f3ff08220d6a8da50e645dbdf053e55c4130504590215d0781314d049881" + }, + { + "path": "lib/vxlan-export.ts", + "sha256": "31c3898ae1d1bb1e2a6de2cf58fdd0ab4278691b1e4350c3235658a57ef5a7af" + }, + { + "path": "lib/vxlan-export.test.ts", + "sha256": "861979c621febc626184a6552def1d37bd9ae79614b8569eaffcfd68fb934d1a" + }, + { + "path": "tests/e2e/vxlan-links.spec.ts", + "sha256": "4748b8791211a0df086b6c006c0b19e5e004a91f1e38162883cf5655565e3816" + }, + { + "path": "tests/e2e/vxlan-table.spec.ts", + "sha256": "4f3c38f684e360442ded047f1d3fe8adce0f59dc47f4e19e3975b2dff2a0619d" + } +] \ No newline at end of file diff --git a/lib/data.ts b/lib/data.ts index b39150ca..6349faff 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -212,6 +212,7 @@ export interface VxlanTunnel { vtepIp: string // VTEP address on the server remoteVteps: string[] // remote VTEP IPs (unicast-flood) l2mtu: number + mtu?: number // actual interface MTU; absent in legacy snapshots arpProxy: boolean arpMode?: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only" macLearning: boolean diff --git a/lib/vxlan-export.test.ts b/lib/vxlan-export.test.ts index 8dcf07eb..1c66d1d1 100644 --- a/lib/vxlan-export.test.ts +++ b/lib/vxlan-export.test.ts @@ -16,3 +16,6 @@ const noLocal = generateVxlanRsc({ ...tunnel, vtepIp: "", arpMode: undefined, ar assert.ok(!noLocal.includes("local-address=")) assert.ok(noLocal.includes('arp="proxy-arp"')) console.log("vxlan-export: quoted strings, supported properties and no invented bridge passed") +assert.match(generateVxlanRsc({ ...tunnel, mtu: 1400 }, {}), /mtu=1400/) +assert.ok(!code.includes("mtu=1500")) +assert.match(code, /MTU в снимке отсутствует/) diff --git a/lib/vxlan-export.ts b/lib/vxlan-export.ts index da7a0c9f..727d815a 100644 --- a/lib/vxlan-export.ts +++ b/lib/vxlan-export.ts @@ -15,7 +15,7 @@ export function generateVxlanRsc(t: VxlanTunnel, servers: Record const server = servers[t.serverId] const lines = [`# VXLAN — ${commentLine(t.name)} · VNI ${t.vni}`] if (server) lines.push(`# Сервер: ${commentLine(server.name)} (${commentLine(server.host)})`) - lines.push("# RouterOS 7.x · интерфейс и статические VTEP; bridge/FDB не включены", "") + lines.push("# RouterOS 7.x · интерфейс и статические VTEP; IP-адреса, bridge/FDB не включены", "") const args = [ `name=${quoteRosString(t.name)}`, `vni=${t.vni}`, `port=${t.dstPort}`, `arp=${quoteRosString(t.arpMode ?? (t.arpProxy ? "proxy-arp" : "enabled"))}`, @@ -23,6 +23,8 @@ export function generateVxlanRsc(t: VxlanTunnel, servers: Record `comment=${quoteRosString(t.comment)}`, ] if (t.vtepIp) args.push(`local-address=${quoteRosString(t.vtepIp)}`) + if (t.mtu !== undefined) args.push(`mtu=${t.mtu}`) + else lines.push("# MTU в снимке отсутствует: проверьте его перед применением") lines.push("/interface/vxlan/add " + args.join(" ")) for (const remote of t.remoteVteps) lines.push(`/interface/vxlan/vteps/add interface=${quoteRosString(t.name)} remote-ip=${quoteRosString(remote)}`) return lines.join("\n") + "\n" diff --git a/lib/vxlan-response.ts b/lib/vxlan-response.ts index 42c72a2c..9044842a 100644 --- a/lib/vxlan-response.ts +++ b/lib/vxlan-response.ts @@ -6,6 +6,7 @@ export const vxlanResponseSchema = z.object({ vni: z.number().int().min(0).max(16777215), port: z.number().int().min(0).max(65535), dstPort: z.number().int().min(1).max(65535), vtepIp: z.string(), remoteVteps: z.array(z.string()), l2mtu: z.number().int().nonnegative(), + mtu: z.number().int().min(1).max(65535).optional(), arpProxy: z.boolean(), arpMode: z.enum(["disabled", "enabled", "local-proxy-arp", "proxy-arp", "reply-only"]).optional(), macLearning: z.boolean(), comment: z.string(), enabled: z.boolean(), status: z.enum(["up", "down"]), })).refine(rows => new Set(rows.map(row => JSON.stringify([row.serverId, row.id]))).size === rows.length && rows.every(row => row.arpMode === undefined || row.arpProxy === (row.arpMode === "proxy-arp" || row.arpMode === "local-proxy-arp"))), diff --git a/tests/e2e/vxlan-links.spec.ts b/tests/e2e/vxlan-links.spec.ts index 032f9684..7b6061a3 100644 --- a/tests/e2e/vxlan-links.spec.ts +++ b/tests/e2e/vxlan-links.spec.ts @@ -174,6 +174,8 @@ test("VXLAN: повреждённые записи не заменяют пос { tunnels: [valid], failures: [{ serverId: "1", error: "Конфликт" }] }, { tunnels: [], failures: [{ serverId: "1", error: "Сбой" }, { serverId: "1", error: "Повтор" }] }, { tunnels: [{ ...valid, arpMode: "enabled", arpProxy: true }] }, + { tunnels: [{ ...valid, mtu: 0 }] }, + { tunnels: [{ ...valid, mtu: "1400" }] }, ]) { control.payload = payload await page.getByRole("button", { name: "Обновить", exact: true }).click() @@ -185,6 +187,21 @@ test("VXLAN: повреждённые записи не заменяют пос await expect(page.getByText("Ошибка загрузки:", { exact: false })).toHaveCount(0) }) +test("VXLAN: меню экспорта сохраняется при обновлении и использует последний MTU", async ({ page }) => { + const control = await setup(page, "/vxlan?server=1") + const tunnel = { id: "stable", serverId: "1", name: "vx-actions", vni: 100, port: 0, dstPort: 4789, vtepIp: "192.0.2.1", remoteVteps: [], l2mtu: 65535, mtu: 1400, arpProxy: false, macLearning: true, comment: "", enabled: true, status: "up" } + control.payload = { tunnels: [tunnel] } + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.locator("tr[data-row-id]")).toHaveCount(1) + await page.getByRole("button", { name: "Действия: vx-actions" }).click() + await expect(page.getByRole("menuitem", { name: "Экспорт .rsc" })).toBeVisible() + control.payload = { tunnels: [{ ...tunnel, mtu: 1380 }] } + await page.getByRole("button", { name: "Обновить", exact: true }).evaluate(button => (button as HTMLButtonElement).click()) + await expect(page.locator("tr[data-row-id]")).toContainText("1380") + await page.getByRole("menuitem", { name: "Экспорт .rsc" }).click() + await expect(page.getByRole("dialog").locator("pre")).toContainText("mtu=1380") +}) + for (const oldFails of [false, true]) { test(`VXLAN: поздний ответ старого устройства игнорируется ${oldFails}`, async ({ page }) => { await setup(page, "/vxlan?server=1") diff --git a/tests/e2e/vxlan-table.spec.ts b/tests/e2e/vxlan-table.spec.ts index 81d3d19c..66bf9d56 100644 --- a/tests/e2e/vxlan-table.spec.ts +++ b/tests/e2e/vxlan-table.spec.ts @@ -4,7 +4,7 @@ import type { VxlanTunnel } from "../../lib/data" test("VXLAN: все строки, числовая сортировка, экспорт выбранной строки и поиск", async ({ page }) => { const tunnels: VxlanTunnel[] = Array.from({ length: 12 }, (_, i) => ({ id: String(i + 1), name: `vxlan-test-${i + 1}`, vni: i + 1, port: 0, dstPort: 8472, - serverId: "1", vtepIp: "192.0.2.1", remoteVteps: [`192.0.2.${i + 2}`], l2mtu: 1500, + serverId: "1", vtepIp: "192.0.2.1", remoteVteps: [`192.0.2.${i + 2}`], l2mtu: 1500, mtu: 1400, arpProxy: false, macLearning: true, comment: "Тест", enabled: true, status: "up", })) const errors: string[] = [] @@ -23,6 +23,7 @@ test("VXLAN: все строки, числовая сортировка, экс await page.getByRole("button", { name: "Действия: vxlan-test-12", exact: true }).click() await page.getByRole("menuitem", { name: "Экспорт .rsc", exact: true }).click() await expect(page.getByRole("dialog")).toContainText("vxlan-test-12") + await expect(page.getByRole("dialog").locator("pre")).toContainText("mtu=1400") await page.keyboard.press("Escape") await page.getByPlaceholder("Поиск по имени, VNI, серверу…").fill("vxlan-test-12") await expect(rows).toHaveCount(1)