fix(vxlan): подтвердить достоверность опроса и экспорта раздела
This commit is contained in:
1 parent
6b068ac39a
commit
e2d81f9657
23 files changed
+689
-89
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
+30
-17
@@ -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
|
||||||
app.get("/vxlan", async (_req, reply) => {
|
interface VxlanDependencies {
|
||||||
return reply.send(await listVxlanResult())
|
listResult: typeof listVxlanResult
|
||||||
})
|
findServer: (id: number) => Promise<ServerRow | undefined>
|
||||||
|
readServer: typeof listVxlanTunnelsForServer
|
||||||
app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
|
||||||
const params = req.params as ServerIdParams
|
|
||||||
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0]
|
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
|
||||||
try {
|
|
||||||
const tunnels = await listVxlanTunnelsForServer(server)
|
|
||||||
return reply.send({ tunnels })
|
|
||||||
} catch {
|
|
||||||
return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
const defaults: VxlanDependencies = {
|
||||||
|
listResult: listVxlanResult,
|
||||||
|
findServer: async id => (await db.select().from(servers).where(eq(servers.id, id)).limit(1))[0],
|
||||||
|
readServer: listVxlanTunnelsForServer,
|
||||||
|
}
|
||||||
|
export function createVxlanRoutes(dependencies: VxlanDependencies = defaults): FastifyPluginAsyncZod {
|
||||||
|
return async (app) => {
|
||||||
|
app.get("/vxlan", async (_req, reply) => {
|
||||||
|
return reply.send(await dependencies.listResult())
|
||||||
|
})
|
||||||
|
|
||||||
export default vxlanRoutes
|
app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||||
|
const params = req.params as ServerIdParams
|
||||||
|
const server = await dependencies.findServer(params.id)
|
||||||
|
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Сервер выключен. Опрос VXLAN не выполнялся" })
|
||||||
|
try {
|
||||||
|
const tunnels = await dependencies.readServer(server)
|
||||||
|
return reply.send({ tunnels })
|
||||||
|
} catch {
|
||||||
|
return reply.status(502).send({ error: "Не удалось опросить VXLAN на сервере" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default createVxlanRoutes()
|
||||||
@@ -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")
|
||||||
@@ -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) }] } }
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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 ограничения остаются в протоколе.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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"))),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user