fix(vxlan): подтвердить достоверность опроса и экспорта раздела

This commit is contained in:
Denozordec committed 2026-10-09 10:38:24 +07:00
1 parent 6b068ac39a
commit e2d81f9657
23 files changed
+679 -79

No files matched your search

+1 -1
View File
@@ -357,7 +357,7 @@ function VxlanPageContent({ selectedServerId, selectedTunnelId, allTunnelsHref,
</DataPageCard> </DataPageCard>
<OpsPanel title="Справка VXLAN" contentClassName="px-5 py-4"> <OpsPanel title="Справка VXLAN" contentClassName="px-5 py-4">
<p className="text-xs text-muted-foreground">Экспорт содержит интерфейс и статические VTEP выбранного туннеля. Bridge и FDB не входят в доступный снимок. <a href="https://help.mikrotik.com/docs/spaces/ROS/pages/100007937/VXLAN" target="_blank" rel="noreferrer" className="underline underline-offset-2">Параметры VXLAN в документации MikroTik</a></p> <p className="text-xs text-muted-foreground">Экспорт содержит интерфейс, известный MTU и статические VTEP выбранного туннеля. IP-адреса, bridge и FDB в него не входят. <a href="https://help.mikrotik.com/docs/spaces/ROS/pages/100007937/VXLAN" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">Параметры VXLAN в документации MikroTik</a></p>
</OpsPanel> </OpsPanel>
</div> </div>
+1 -1
View File
@@ -28,7 +28,7 @@
"test:ros-file": "tsx src/services/ros-file-contents.test.ts", "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: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: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": "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: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", "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",
+34
View File
@@ -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")
+20 -7
View File
@@ -5,22 +5,35 @@ import { servers } from "../db/schema.js"
import { listVxlanResult, listVxlanTunnelsForServer } from "../services/vxlan-live.js" import { listVxlanResult, listVxlanTunnelsForServer } from "../services/vxlan-live.js"
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js" import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
const vxlanRoutes: FastifyPluginAsyncZod = async (app) => { type ServerRow = typeof servers.$inferSelect
interface VxlanDependencies {
listResult: typeof listVxlanResult
findServer: (id: number) => Promise<ServerRow | undefined>
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) => { app.get("/vxlan", async (_req, reply) => {
return reply.send(await listVxlanResult()) return reply.send(await dependencies.listResult())
}) })
app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => { app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams const params = req.params as ServerIdParams
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0] const server = await dependencies.findServer(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" }) if (!server) return reply.status(404).send({ error: "Сервер не найден" })
if (!server.enabled) return reply.status(409).send({ error: "Сервер выключен. Опрос VXLAN не выполнялся" })
try { try {
const tunnels = await listVxlanTunnelsForServer(server) const tunnels = await dependencies.readServer(server)
return reply.send({ tunnels }) return reply.send({ tunnels })
} catch { } catch {
return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" }) return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" })
} }
}) })
}
} }
export default createVxlanRoutes()
export default vxlanRoutes
+24 -3
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { mapVxlanRow, pollVxlanServers } from "./vxlan-live.js" import { mapVxlanRow, mapVxlanSnapshot, pollVxlanServers, listVxlanTunnelsForServer } from "./vxlan-live.js"
const server = { const server = {
id: 7, id: 7,
@@ -20,6 +20,7 @@ const row = mapVxlanRow(
running: "true", running: "true",
disabled: "false", disabled: "false",
l2mtu: "1500", l2mtu: "1500",
mtu: "1400",
"mac-learning": "true", "mac-learning": "true",
"arp-proxy": "true", "arp-proxy": "true",
comment: "overlay", comment: "overlay",
@@ -55,12 +56,32 @@ assert.equal(failed.tunnels.length, 0)
assert.equal(failed.failures.length, 1) assert.equal(failed.failures.length, 1)
console.log("vxlan polling: mixed/empty/all-failed ok") 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) { 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.arpMode, arp)
assert.equal(mapped.arpProxy, arp === "proxy-arp" || arp === "local-proxy-arp") assert.equal(mapped.arpProxy, arp === "proxy-arp" || arp === "local-proxy-arp")
assert.equal(mapped.macLearning, false) assert.equal(mapped.macLearning, false)
} }
console.log("vxlan: exact ARP modes and RouterOS no-learning passed") 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<string, string>
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")
+48 -20
View File
@@ -1,4 +1,5 @@
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js" import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
import { isIP } from "node:net"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { db } from "../db/index.js" import { db } from "../db/index.js"
import { servers } from "../db/schema.js" import { servers } from "../db/schema.js"
@@ -17,6 +18,7 @@ interface RosVxlan {
disabled?: string disabled?: string
comment?: string comment?: string
l2mtu?: string l2mtu?: string
mtu?: string
arp?: string arp?: string
"arp-proxy"?: string "arp-proxy"?: string
"mac-learning"?: string "mac-learning"?: string
@@ -40,6 +42,7 @@ export type VxlanTunnelLive = {
vtepIp: string vtepIp: string
remoteVteps: string[] remoteVteps: string[]
l2mtu: number l2mtu: number
mtu: number
arpProxy: boolean arpProxy: boolean
arpMode: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only" arpMode: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only"
macLearning: boolean macLearning: boolean
@@ -48,45 +51,55 @@ export type VxlanTunnelLive = {
status: "up" | "down" status: "up" | "down"
} }
function rosYes(v: string | undefined): boolean { function rosYes(v: unknown): boolean {
return v === "true" || v === "yes" return v === true || v === "true" || v === "yes"
} }
function parseIntSafe(v: string | undefined, fallback: number): number { function readInteger(value: unknown, field: string, min: number, max: number): number {
const n = Number.parseInt(v ?? "", 10) const n = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : typeof value === "number" ? value : NaN
return Number.isFinite(n) ? n : fallback 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( export function mapVxlanRow(
server: ServerRow, server: ServerRow,
vx: RosVxlan, vx: RosVxlan,
vteps: RosVxlanVtep[], vteps: RosVxlanVtep[],
idx: number, _idx: number,
): VxlanTunnelLive { ): VxlanTunnelLive {
const name = (vx.name ?? "").trim() || `vxlan-${idx + 1}` const name = typeof vx?.name === "string" ? vx.name.trim() : ""
const rosId = String(vx[".id"] ?? name) const rosId = typeof vx?.[".id"] === "string" ? vx[".id"].trim() : ""
const disabled = rosYes(vx.disabled) if (!name || !rosId) throw new Error("Некорректный снимок VXLAN: имя или ID интерфейса")
const running = rosYes(vx.running) const disabled = readFlag(vx.disabled, "disabled")
const port = parseIntSafe(vx.port, 8472) const running = readFlag(vx.running, "running")
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 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 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()) .map((v) => (v["remote-ip"] ?? "").trim())
.filter(Boolean) .filter(Boolean)
return { return {
id: `${server.id}-${rosId}`, id: `${server.id}-${rosId}`,
rosId, rosId,
name, name,
vni: parseIntSafe(vx.vni, 0), vni: readInteger(vx.vni, "vni", 1, 16777215),
port: 0, port: 0,
dstPort: port, dstPort: port,
serverId: String(server.id), serverId: String(server.id),
vtepIp: (vx["local-address"] ?? vx["vtep-address"] ?? "").trim(), vtepIp: (vx["local-address"] ?? vx["vtep-address"] ?? "").trim(),
remoteVteps, 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", arpProxy: arpMode === "proxy-arp" || arpMode === "local-proxy-arp",
arpMode, 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 ?? "", comment: vx.comment ?? "",
enabled: !disabled, enabled: !disabled,
status: !disabled && running ? "up" : "down", status: !disabled && running ? "up" : "down",
@@ -94,22 +107,37 @@ export function mapVxlanRow(
} }
async function fetchVxlanForServer(server: ServerRow): Promise<VxlanTunnelLive[]> { async function fetchVxlanForServer(server: ServerRow): Promise<VxlanTunnelLive[]> {
if (!server.enabled) throw new Error("Сервер выключен. Опрос VXLAN не выполнялся")
const client = MikrotikClient.fromServer(server) const client = MikrotikClient.fromServer(server)
const [vxRaw, vtepRaw] = await Promise.all([ const [vxRaw, vtepRaw] = await Promise.all([
client.get<RosVxlan[]>("/interface/vxlan"), client.get<RosVxlan[]>("/interface/vxlan"),
client.get<RosVxlanVtep[]>("/interface/vxlan/vteps"), client.get<RosVxlanVtep[]>("/interface/vxlan/vteps"),
]) ])
if (!Array.isArray(vxRaw) || !Array.isArray(vtepRaw)) throw new Error("Некорректный ответ VXLAN устройства") if (!Array.isArray(vxRaw) || !Array.isArray(vtepRaw)) throw new Error("Некорректный ответ VXLAN устройства")
const list = vxRaw return mapVxlanSnapshot(server, vxRaw, vtepRaw)
const vteps = vtepRaw }
return list.map((vx, idx) => mapVxlanRow(server, vx, vteps, idx))
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( export async function pollVxlanServers(
serverRows: ServerRow[], serverRows: ServerRow[],
fetchServer: (server: ServerRow) => Promise<VxlanTunnelLive[]> = fetchVxlanForServer, fetchServer: (server: ServerRow) => Promise<VxlanTunnelLive[]> = fetchVxlanForServer,
): Promise<{ tunnels: VxlanTunnelLive[]; failures: Array<{ serverId: string; serverName: string; error: string }> }> { ): 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: [] } } 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) }] } } catch (error) { return { tunnels: [], failures: [{ serverId: String(server.id), serverName: server.name, error: error instanceof Error ? error.message : String(error) }] } }
})) }))
+54 -34
View File
@@ -1,6 +1,6 @@
"use client" "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 { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { import {
type ColumnDef, type ColumnDef,
@@ -37,6 +37,43 @@ interface VxlanDataGridProps {
onExport: (tunnel: VxlanTunnel) => void onExport: (tunnel: VxlanTunnel) => void
} }
const VxlanActionsContext = createContext<VxlanDataGridProps["onExport"] | null>(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 (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"transition-colors",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${tunnel.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(tunnel)}>
<CodeXmlIcon className="size-4" />
Экспорт .rsc
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: VxlanDataGridProps) { function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: VxlanDataGridProps) {
const serverMap = useMemo( const serverMap = useMemo(
() => new Map(servers.map((s) => [s.id, s])), () => 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"), cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
}, },
}, },
{
id: "mtu",
size: 88,
accessorKey: "mtu",
header: ({ column }) => <DataGridSortHeader column={column} title="MTU" />,
cell: ({ row }) => <span className="font-mono text-sm">{row.original.mtu ?? "—"}</span>,
meta: {
headerTitle: "MTU",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{ {
id: "remoteVteps", id: "remoteVteps",
size: 160, size: 160,
@@ -204,38 +253,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl
{ {
id: "actions", id: "actions",
header: () => <span className="sr-only">Действия</span>, header: () => <span className="sr-only">Действия</span>,
cell: ({ row }) => { cell: VxlanActionsCell,
const tunnel = row.original
return (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"transition-colors",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${tunnel.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(tunnel)}>
<CodeXmlIcon className="size-4" />
Экспорт .rsc
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
enableSorting: false, enableSorting: false,
size: 72, size: 72,
meta: { meta: {
@@ -244,7 +262,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl
}, },
}, },
], ],
[onExport, serverMap, failedServerIds], [serverMap, failedServerIds],
) )
const table = useTable({ const table = useTable({
@@ -275,6 +293,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl
} }
return ( return (
<VxlanActionsContext.Provider value={onExport}>
<DataGridShell <DataGridShell
table={table} table={table}
recordCount={tunnels.length} recordCount={tunnels.length}
@@ -289,6 +308,7 @@ function VxlanDataGrid({ tunnels, servers, onExport, failedServerIds = [] }: Vxl
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea> <DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer> </DataGridContainer>
</DataGridShell> </DataGridShell>
</VxlanActionsContext.Provider>
) )
} }
+35 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5 # 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 приёмки. Финальный 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. По 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 ограничения остаются в протоколе.
+41 -5
View File
@@ -1,14 +1,16 @@
# План реализации UX/UI по пятичасовым окнам # План реализации 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. Обнаружен настоящий дефект: принятый 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. Официальная схема пары и 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 ограничения остаются в протоколе.
+38 -2
View File
@@ -1,11 +1,13 @@
# UX/UI: журнал реализации # 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). Общий план: [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/связность и очищать только созданные объекты, сохраняя исходную конфигурацию. - По поручению2026-10-08 при внешнем тестировании VPN создавать реальные собственные тестовые туннели, особенно GRE; проверять API/RouterOS/связность и очищать только созданные объекты, сохраняя исходную конфигурацию.
- План: ReUI PRO, Frame, base-nova; KpiStatGrid, ServerTileRail и CodeExportSheet остаются источниками общих композиционных решений. - План: ReUI PRO, Frame, base-nova; KpiStatGrid, ServerTileRail и CodeExportSheet остаются источниками общих композиционных решений.
@@ -34,7 +36,41 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 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) ## 12.5.75 — собственная VXLAN пара и реальный каталог/экспорт (2026-10-09)
+35 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI # Этап 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. План: [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 приёмки. Финальный 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. По 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 ограничения остаются в протоколе.
+36 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-09 # Тестовый сервер 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 | | PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data | | Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска | | Доступ | 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). 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 приёмки. Финальный 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. По 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 ограничения остаются в протоколе.
@@ -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"
]
}
@@ -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": []
}
@@ -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
}
@@ -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"
]
}
@@ -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"
}
]
+1
View File
@@ -212,6 +212,7 @@ export interface VxlanTunnel {
vtepIp: string // VTEP address on the server vtepIp: string // VTEP address on the server
remoteVteps: string[] // remote VTEP IPs (unicast-flood) remoteVteps: string[] // remote VTEP IPs (unicast-flood)
l2mtu: number l2mtu: number
mtu?: number // actual interface MTU; absent in legacy snapshots
arpProxy: boolean arpProxy: boolean
arpMode?: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only" arpMode?: "disabled" | "enabled" | "local-proxy-arp" | "proxy-arp" | "reply-only"
macLearning: boolean macLearning: boolean
+3
View File
@@ -16,3 +16,6 @@ const noLocal = generateVxlanRsc({ ...tunnel, vtepIp: "", arpMode: undefined, ar
assert.ok(!noLocal.includes("local-address=")) assert.ok(!noLocal.includes("local-address="))
assert.ok(noLocal.includes('arp="proxy-arp"')) assert.ok(noLocal.includes('arp="proxy-arp"'))
console.log("vxlan-export: quoted strings, supported properties and no invented bridge passed") 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 в снимке отсутствует/)
+3 -1
View File
@@ -15,7 +15,7 @@ export function generateVxlanRsc(t: VxlanTunnel, servers: Record<string, Server>
const server = servers[t.serverId] const server = servers[t.serverId]
const lines = [`# VXLAN — ${commentLine(t.name)} · VNI ${t.vni}`] const lines = [`# VXLAN — ${commentLine(t.name)} · VNI ${t.vni}`]
if (server) lines.push(`# Сервер: ${commentLine(server.name)} (${commentLine(server.host)})`) 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 = [ const args = [
`name=${quoteRosString(t.name)}`, `vni=${t.vni}`, `port=${t.dstPort}`, `name=${quoteRosString(t.name)}`, `vni=${t.vni}`, `port=${t.dstPort}`,
`arp=${quoteRosString(t.arpMode ?? (t.arpProxy ? "proxy-arp" : "enabled"))}`, `arp=${quoteRosString(t.arpMode ?? (t.arpProxy ? "proxy-arp" : "enabled"))}`,
@@ -23,6 +23,8 @@ export function generateVxlanRsc(t: VxlanTunnel, servers: Record<string, Server>
`comment=${quoteRosString(t.comment)}`, `comment=${quoteRosString(t.comment)}`,
] ]
if (t.vtepIp) args.push(`local-address=${quoteRosString(t.vtepIp)}`) 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(" ")) 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)}`) 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" return lines.join("\n") + "\n"
+1
View File
@@ -6,6 +6,7 @@ export const vxlanResponseSchema = z.object({
vni: z.number().int().min(0).max(16777215), vni: z.number().int().min(0).max(16777215),
port: z.number().int().min(0).max(65535), dstPort: z.number().int().min(1).max(65535), 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(), 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(), 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"]), 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"))), })).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"))),
+17
View File
@@ -174,6 +174,8 @@ test("VXLAN: повреждённые записи не заменяют пос
{ tunnels: [valid], failures: [{ serverId: "1", error: "Конфликт" }] }, { tunnels: [valid], failures: [{ serverId: "1", error: "Конфликт" }] },
{ tunnels: [], failures: [{ serverId: "1", error: "Сбой" }, { serverId: "1", error: "Повтор" }] }, { tunnels: [], failures: [{ serverId: "1", error: "Сбой" }, { serverId: "1", error: "Повтор" }] },
{ tunnels: [{ ...valid, arpMode: "enabled", arpProxy: true }] }, { tunnels: [{ ...valid, arpMode: "enabled", arpProxy: true }] },
{ tunnels: [{ ...valid, mtu: 0 }] },
{ tunnels: [{ ...valid, mtu: "1400" }] },
]) { ]) {
control.payload = payload control.payload = payload
await page.getByRole("button", { name: "Обновить", exact: true }).click() await page.getByRole("button", { name: "Обновить", exact: true }).click()
@@ -185,6 +187,21 @@ test("VXLAN: повреждённые записи не заменяют пос
await expect(page.getByText("Ошибка загрузки:", { exact: false })).toHaveCount(0) 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]) { for (const oldFails of [false, true]) {
test(`VXLAN: поздний ответ старого устройства игнорируется ${oldFails}`, async ({ page }) => { test(`VXLAN: поздний ответ старого устройства игнорируется ${oldFails}`, async ({ page }) => {
await setup(page, "/vxlan?server=1") await setup(page, "/vxlan?server=1")
+2 -1
View File
@@ -4,7 +4,7 @@ import type { VxlanTunnel } from "../../lib/data"
test("VXLAN: все строки, числовая сортировка, экспорт выбранной строки и поиск", async ({ page }) => { test("VXLAN: все строки, числовая сортировка, экспорт выбранной строки и поиск", async ({ page }) => {
const tunnels: VxlanTunnel[] = Array.from({ length: 12 }, (_, i) => ({ const tunnels: VxlanTunnel[] = Array.from({ length: 12 }, (_, i) => ({
id: String(i + 1), name: `vxlan-test-${i + 1}`, vni: i + 1, port: 0, dstPort: 8472, 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", arpProxy: false, macLearning: true, comment: "Тест", enabled: true, status: "up",
})) }))
const errors: string[] = [] const errors: string[] = []
@@ -23,6 +23,7 @@ test("VXLAN: все строки, числовая сортировка, экс
await page.getByRole("button", { name: "Действия: vxlan-test-12", exact: true }).click() await page.getByRole("button", { name: "Действия: vxlan-test-12", exact: true }).click()
await page.getByRole("menuitem", { name: "Экспорт .rsc", 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")).toContainText("vxlan-test-12")
await expect(page.getByRole("dialog").locator("pre")).toContainText("mtu=1400")
await page.keyboard.press("Escape") await page.keyboard.press("Escape")
await page.getByPlaceholder("Поиск по имени, VNI, серверу…").fill("vxlan-test-12") await page.getByPlaceholder("Поиск по имени, VNI, серверу…").fill("vxlan-test-12")
await expect(rows).toHaveCount(1) await expect(rows).toHaveCount(1)