fix(routes): подтвердить применение и восстановление рекурсивных маршрутов
This commit is contained in:
1 parent
36c6a21ca5
commit
5b3133ce73
19 files changed
+659
-28
No files matched your search
@@ -23,7 +23,7 @@
|
|||||||
"test:traffic-flow": "tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
|
"test:traffic-flow": "tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
|
||||||
"test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
|
"test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
|
||||||
"test:pg": "tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts",
|
"test:pg": "tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts",
|
||||||
"test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts",
|
"test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-apply.test.ts",
|
||||||
"test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts",
|
"test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts",
|
||||||
"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",
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import assert from "node:assert/strict"
|
|||||||
import Fastify from "fastify"
|
import Fastify from "fastify"
|
||||||
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
||||||
import { createRecursiveRoutes, type RecursiveRouteDependencies } from "./recursive-routes.js"
|
import { createRecursiveRoutes, type RecursiveRouteDependencies } from "./recursive-routes.js"
|
||||||
|
import { RecursiveApplyError } from "../services/recursive-apply.js"
|
||||||
const row = { id: "one", dstAddress: "10.0.0.0/24", gateway: "192.0.2.1", distance: 0, scope: 0, targetScope: 0, routingTable: "main", checkGateway: "ping", country: "", comment: "Маршрут", disabled: false }
|
const row = { id: "one", dstAddress: "10.0.0.0/24", gateway: "192.0.2.1", distance: 0, scope: 0, targetScope: 0, routingTable: "main", checkGateway: "ping", country: "", comment: "Маршрут", disabled: false }
|
||||||
const server = { id: 1, name: "Тест" } as NonNullable<Awaited<ReturnType<RecursiveRouteDependencies["getServer"]>>>
|
const server = { id: 1, name: "Тест", enabled: true, type: "home-router" } as NonNullable<Awaited<ReturnType<RecursiveRouteDependencies["getServer"]>>>
|
||||||
|
let rawReads = 0, lateReadFailure = false, applyError: Error | undefined
|
||||||
let lookup = 0, writes = 0, revisions = 0, fail = false, stored = [row], payload: unknown = [row], revisionServer = 1, section = "recursive-routes"
|
let lookup = 0, writes = 0, revisions = 0, fail = false, stored = [row], payload: unknown = [row], revisionServer = 1, section = "recursive-routes"
|
||||||
const observed = { ".id": "*1", static: "true", "dst-address": row.dstAddress, gateway: row.gateway, distance: "0", scope: "0", "target-scope": "0", "routing-table": "main", "check-gateway": "ping", disabled: "false", comment: "MikrotikManager:recursive Тест" }
|
const observed = { ".id": "*1", static: "true", "dst-address": row.dstAddress, gateway: row.gateway, distance: "0", scope: "0", "target-scope": "0", "routing-table": "main", "check-gateway": "ping", disabled: "false", comment: "MikrotikManager:recursive Тест" }
|
||||||
let rawPayload: unknown = [observed], rawFailure = false, savedSnapshots = 0
|
let rawPayload: unknown = [observed], rawFailure = false, savedSnapshots = 0
|
||||||
@@ -12,12 +14,13 @@ const app = Fastify()
|
|||||||
app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
|
app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
|
||||||
await app.register(createRecursiveRoutes({
|
await app.register(createRecursiveRoutes({
|
||||||
getServer: async id => { lookup++; return id === 1 ? server : undefined },
|
getServer: async id => { lookup++; return id === 1 ? server : undefined },
|
||||||
getRoutes: async () => stored,
|
getRoutes: async () => { if (lateReadFailure) throw Error("Сбой чтения после применения"); return stored },
|
||||||
readRaw: async () => { if (rawFailure) throw new Error("Тест опроса"); return rawPayload },
|
readRaw: async () => { rawReads++; if (rawFailure) throw new Error("Тест опроса"); return rawPayload },
|
||||||
saveObserved: async (_id, rows) => { savedSnapshots++; stored = rows as typeof row[] },
|
saveObserved: async (_id, rows) => { savedSnapshots++; stored = rows as typeof row[] },
|
||||||
apply: async (device, rows, source) => {
|
apply: async (device, rows, source) => {
|
||||||
assert.equal(device.id, 1); writes++; applied.push({ rows: rows as typeof row[], source })
|
assert.equal(device.id, 1); writes++; applied.push({ rows: rows as typeof row[], source })
|
||||||
if (fail) throw new Error("Тест частичного применения")
|
if (fail) throw new Error("Тест частичного применения")
|
||||||
|
if (applyError) throw applyError
|
||||||
stored = rows as typeof row[]; return { pushed: rows.length, deleted: 0 }
|
stored = rows as typeof row[]; return { pushed: rows.length, deleted: 0 }
|
||||||
},
|
},
|
||||||
getRevision: async () => {
|
getRevision: async () => {
|
||||||
@@ -32,6 +35,7 @@ const invalid = [
|
|||||||
{ serverId: 1, routes: [{ ...row, distance: -1 }] }, { serverId: 1, routes: [{ ...row, scope: 256 }] },
|
{ serverId: 1, routes: [{ ...row, distance: -1 }] }, { serverId: 1, routes: [{ ...row, scope: 256 }] },
|
||||||
{ serverId: 1, routes: [{ ...row, targetScope: 1.5 }] }, { serverId: 1, routes: [{ ...row, gateway: "" }] },
|
{ serverId: 1, routes: [{ ...row, targetScope: 1.5 }] }, { serverId: 1, routes: [{ ...row, gateway: "" }] },
|
||||||
{ serverId: 1, routes: [row, row] }, { serverId: 1, routes: [row], source: "rollback" },
|
{ serverId: 1, routes: [row, row] }, { serverId: 1, routes: [row], source: "rollback" },
|
||||||
|
...[{...row,dstAddress:"999.1.1.1/24"},{...row,dstAddress:"10.0.0.0/33"},{...row,gateway:"ether1"},{...row,gateway:"192.0.2.1%"},{...row,checkGateway:"anything"}].map(route=>({serverId:1,routes:[route]})),
|
||||||
]
|
]
|
||||||
for (const body of invalid) {
|
for (const body of invalid) {
|
||||||
const result = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: body })
|
const result = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: body })
|
||||||
@@ -83,6 +87,22 @@ rawFailure=false;rawPayload=[]
|
|||||||
assert.deepEqual((await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})).json(),{serverId:1,routes:[],live:true,stale:false})
|
assert.deepEqual((await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})).json(),{serverId:1,routes:[],live:true,stale:false})
|
||||||
rawPayload=[{".id":"one",gateway:"192.0.2.1"}]
|
rawPayload=[{".id":"one",gateway:"192.0.2.1"}]
|
||||||
assert.equal((await app.inject({method:"GET",url:"/api/recursive-routes/gateways/catalog?serverId=1"})).json().gateways[0].status,"unknown")
|
assert.equal((await app.inject({method:"GET",url:"/api/recursive-routes/gateways/catalog?serverId=1"})).json().gateways[0].status,"unknown")
|
||||||
|
for (const type of ["home-router", "exit-node"] as const) {
|
||||||
|
server.enabled=false;server.type=type
|
||||||
|
const counts={writes,rawReads,savedSnapshots}
|
||||||
|
for(const url of ["/recursive-routes/catalog?serverId=1","/recursive-routes/gateways/catalog?serverId=1","/recursive-routes?serverId=1","/recursive-routes/gateways?serverId=1"])assert.equal((await app.inject({method:"GET",url:"/api"+url})).statusCode,409)
|
||||||
|
assert.equal((await app.inject({method:"PUT",url:"/api/recursive-routes",payload:{serverId:1,routes:[]}})).statusCode,409)
|
||||||
|
assert.equal((await app.inject({method:"POST",url:"/api/recursive-routes/revisions/rev/restore",payload:{serverId:1}})).statusCode,409)
|
||||||
|
assert.deepEqual({writes,rawReads,savedSnapshots},counts)
|
||||||
|
}
|
||||||
|
server.enabled=true
|
||||||
|
for(const state of ["partial","unknown","applied"] as const){
|
||||||
|
applyError=new RecursiveApplyError("Сбой операции",{state,confirmedWrites:1,createdIds:["*1"],deletedIds:[],needsReview:true,metadataRecorded:false})
|
||||||
|
for(const restore of [false,true]){const r=await app.inject({method:restore?"POST":"PUT",url:restore?"/api/recursive-routes/revisions/rev/restore":"/api/recursive-routes",payload:restore?{serverId:1}:{serverId:1,routes:[]}});assert.equal(r.statusCode,502);assert.equal(r.json().state,state);assert.equal(r.json().confirmedWrites,1);assert.equal(r.json().ok,undefined)}
|
||||||
|
}
|
||||||
|
applyError=undefined;lateReadFailure=true
|
||||||
|
const late=await app.inject({method:"PUT",url:"/api/recursive-routes",payload:{serverId:1,routes:[row]}});assert.equal(late.statusCode,502);assert.equal(late.json().state,"applied");assert.equal(late.json().metadataRecorded,true)
|
||||||
|
lateReadFailure=false
|
||||||
await app.close()
|
await app.close()
|
||||||
console.log("recursive routes HTTP: invalid/explicit-empty/zero/copy/error/restore ownership and payload passed without DB/RouterOS")
|
console.log("recursive routes HTTP: invalid/explicit-empty/zero/copy/error/restore ownership and payload passed without DB/RouterOS")
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { recursiveApplyBodySchema, recursiveTargetSchema, recursiveInputRowsSchema } from '../services/recursive-route-validation.js'
|
import { recursiveApplyBodySchema, recursiveTargetSchema, recursiveInputRowsSchema } from '../services/recursive-route-validation.js'
|
||||||
import { parseRecursiveCatalog, parseRecursiveGatewayCatalog } from "../services/recursive-route-catalog.js"
|
import { parseRecursiveCatalog, parseRecursiveGatewayCatalog } from "../services/recursive-route-catalog.js"
|
||||||
|
import { executeRecursiveApply, RecursiveApplyError } from "../services/recursive-apply.js"
|
||||||
import { asc, eq } from "drizzle-orm"
|
import { asc, eq } from "drizzle-orm"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
@@ -10,7 +11,6 @@ import { parseDbServerId } from "../utils/server-id.js"
|
|||||||
import { managedRecursiveComment } from "../managed-markers.js"
|
import { managedRecursiveComment } from "../managed-markers.js"
|
||||||
import {
|
import {
|
||||||
mapRosManagedRoutes,
|
mapRosManagedRoutes,
|
||||||
planRecursiveApply,
|
|
||||||
userRecursiveComment,
|
userRecursiveComment,
|
||||||
} from "../services/config-apply-plan.js"
|
} from "../services/config-apply-plan.js"
|
||||||
import {
|
import {
|
||||||
@@ -123,10 +123,11 @@ function toRouterPayload(route: RecursiveRouteDto): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
|
async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
|
||||||
await db.delete(recursiveRoutes).where(eq(recursiveRoutes.serverId, serverId))
|
await db.transaction(async tx => {
|
||||||
|
await tx.delete(recursiveRoutes).where(eq(recursiveRoutes.serverId, serverId))
|
||||||
if (routes.length === 0) return
|
if (routes.length === 0) return
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
await db.insert(recursiveRoutes).values(
|
await tx.insert(recursiveRoutes).values(
|
||||||
routes.map((r, i) => ({
|
routes.map((r, i) => ({
|
||||||
serverId,
|
serverId,
|
||||||
sortOrder: i,
|
sortOrder: i,
|
||||||
@@ -144,6 +145,7 @@ async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyRecursiveToServer(
|
async function applyRecursiveToServer(
|
||||||
@@ -152,16 +154,7 @@ async function applyRecursiveToServer(
|
|||||||
source: ConfigRevisionSource,
|
source: ConfigRevisionSource,
|
||||||
): Promise<{ pushed: number; deleted: number }> {
|
): Promise<{ pushed: number; deleted: number }> {
|
||||||
const client = MikrotikClient.fromServer(server)
|
const client = MikrotikClient.fromServer(server)
|
||||||
const existing = await client.get<RosRoute[]>("/ip/route")
|
return executeRecursiveApply(client, routes.map(toRouterPayload), async () => {
|
||||||
const { deleteIds } = planRecursiveApply(existing)
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
await client.post("/ip/route", toRouterPayload(route))
|
|
||||||
}
|
|
||||||
for (const id of deleteIds) {
|
|
||||||
await client.delete(`/ip/route/${encodeURIComponent(id)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await replaceDbRoutes(server.id, routes)
|
await replaceDbRoutes(server.id, routes)
|
||||||
await appendRevisionIfChanged({
|
await appendRevisionIfChanged({
|
||||||
serverId: server.id,
|
serverId: server.id,
|
||||||
@@ -169,11 +162,18 @@ async function applyRecursiveToServer(
|
|||||||
source,
|
source,
|
||||||
payload: canonicalRecursiveRoutes(routes),
|
payload: canonicalRecursiveRoutes(routes),
|
||||||
})
|
})
|
||||||
return { pushed: routes.length, deleted: deleteIds.length }
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const RevisionIdParamSchema = z.object({ id: z.string().min(1) })
|
const RevisionIdParamSchema = z.object({ id: z.string().min(1) })
|
||||||
|
|
||||||
|
function recursiveFailureReceipt(error: unknown, result?: { pushed: number; deleted: number }) {
|
||||||
|
if (error instanceof RecursiveApplyError) return error.receipt
|
||||||
|
// Native operations and metadata completed; a later cache read can still fail.
|
||||||
|
if (result) return { state: "applied", confirmedWrites: result.pushed + result.deleted, needsReview: true, metadataRecorded: true }
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export interface RecursiveRouteDependencies {
|
export interface RecursiveRouteDependencies {
|
||||||
getServer: (id: number) => Promise<ServerRow | undefined>
|
getServer: (id: number) => Promise<ServerRow | undefined>
|
||||||
readRaw: (server: ServerRow) => Promise<unknown>
|
readRaw: (server: ServerRow) => Promise<unknown>
|
||||||
@@ -201,6 +201,7 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
if (!parsed.success) return reply.status(400).send({ error: "serverId is required" })
|
if (!parsed.success) return reply.status(400).send({ error: "serverId is required" })
|
||||||
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
|
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
try {
|
try {
|
||||||
const routes = mergeCachedCountry(parseRecursiveCatalog(await deps.readRaw(server)), await deps.getRoutes(serverId))
|
const routes = mergeCachedCountry(parseRecursiveCatalog(await deps.readRaw(server)), await deps.getRoutes(serverId))
|
||||||
await deps.saveObserved(serverId, routes)
|
await deps.saveObserved(serverId, routes)
|
||||||
@@ -214,6 +215,7 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
if (!parsed.success) return reply.status(400).send({ error: "serverId is required" })
|
if (!parsed.success) return reply.status(400).send({ error: "serverId is required" })
|
||||||
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
|
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
try {
|
try {
|
||||||
return reply.send({ serverId, gateways: parseRecursiveGatewayCatalog(await deps.readRaw(server)) })
|
return reply.send({ serverId, gateways: parseRecursiveGatewayCatalog(await deps.readRaw(server)) })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -227,6 +229,7 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
|
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
|
||||||
const server = await deps.getServer(serverId)
|
const server = await deps.getServer(serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = MikrotikClient.fromServer(server)
|
const client = MikrotikClient.fromServer(server)
|
||||||
@@ -257,6 +260,7 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
|
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
|
||||||
const server = await deps.getServer(serverId)
|
const server = await deps.getServer(serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
|
|
||||||
const cached = await deps.getRoutes(serverId)
|
const cached = await deps.getRoutes(serverId)
|
||||||
try {
|
try {
|
||||||
@@ -290,14 +294,17 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
const body = parsed.data, serverId = body.serverId
|
const body = parsed.data, serverId = body.serverId
|
||||||
const server = await deps.getServer(serverId)
|
const server = await deps.getServer(serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
const routes = body.routes.map(r => ({ ...r, comment: userRecursiveComment(r.comment) }))
|
const routes = body.routes.map(r => ({ ...r, comment: userRecursiveComment(r.comment) }))
|
||||||
const source: ConfigRevisionSource = body.source === "copy" ? "copy" : "apply"
|
const source: ConfigRevisionSource = body.source === "copy" ? "copy" : "apply"
|
||||||
|
let result: { pushed: number; deleted: number } | undefined
|
||||||
try {
|
try {
|
||||||
const result = await deps.apply(server, routes, source)
|
result = await deps.apply(server, routes, source)
|
||||||
const stored = await deps.getRoutes(serverId)
|
const stored = await deps.getRoutes(serverId)
|
||||||
return reply.send({ ok: true, serverId, routes: stored, pushedRoutes: result.pushed })
|
return reply.send({ ok: true, serverId, routes: stored, pushedRoutes: result.pushed })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reply.status(500).send({ error: String(err) })
|
const receipt = recursiveFailureReceipt(err, result)
|
||||||
|
return reply.status(receipt ? 502 : 500).send({ error: String(err), serverId, ...receipt })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -323,15 +330,18 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
|
|||||||
if (!parsedRoutes.success) return reply.status(409).send({ error: "Версия содержит некорректные маршруты; восстановление не выполнено" })
|
if (!parsedRoutes.success) return reply.status(409).send({ error: "Версия содержит некорректные маршруты; восстановление не выполнено" })
|
||||||
const server = await deps.getServer(rev.serverId)
|
const server = await deps.getServer(rev.serverId)
|
||||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||||
|
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
|
||||||
const routes: RecursiveRouteDto[] = parsedRoutes.data.map((row, index) => ({
|
const routes: RecursiveRouteDto[] = parsedRoutes.data.map((row, index) => ({
|
||||||
...row, id: `rev-${index}`, comment: userRecursiveComment(row.comment),
|
...row, id: `rev-${index}`, comment: userRecursiveComment(row.comment),
|
||||||
}))
|
}))
|
||||||
|
let result: { pushed: number; deleted: number } | undefined
|
||||||
try {
|
try {
|
||||||
const result = await deps.apply(server, routes, "rollback")
|
result = await deps.apply(server, routes, "rollback")
|
||||||
const stored = await deps.getRoutes(server.id)
|
const stored = await deps.getRoutes(server.id)
|
||||||
return reply.send({ ok: true, serverId: server.id, routes: stored, pushedRoutes: result.pushed })
|
return reply.send({ ok: true, serverId: server.id, routes: stored, pushedRoutes: result.pushed })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return reply.status(500).send({ error: String(err) })
|
const receipt = recursiveFailureReceipt(err, result)
|
||||||
|
return reply.status(receipt ? 502 : 500).send({ error: String(err), serverId: server.id, ...receipt })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import type { MikrotikClient } from "./mikrotik.js"
|
||||||
|
import { MikrotikError } from "./mikrotik.js"
|
||||||
|
import { executeRecursiveApply, RecursiveApplyError } from "./recursive-apply.js"
|
||||||
|
const owned = { ".id": "*old", static: "true", "dst-address": "198.18.88.0/24", gateway: "192.0.2.1", distance: "1", scope: "30", "target-scope": "10", "routing-table": "main", "check-gateway": "none", disabled: "false", comment: "MikrotikManager:recursive test" }
|
||||||
|
let rows: unknown = [owned, { ".id": "*foreign", comment: "Keep default route" }], events: string[] = [], puts = 0
|
||||||
|
let failAt = 0, rejection: Error = new Error("ACK потерян"), badAck = false, failDelete = false, failPersist = false
|
||||||
|
const client: Pick<MikrotikClient,"get"|"put"|"delete"> = {
|
||||||
|
get: async <T>() => rows as T,
|
||||||
|
put: async <T>() => { puts++; events.push("put:"+puts); if(puts===failAt)throw rejection; return (badAck?{}:{".id":"*new"+puts}) as T },
|
||||||
|
delete: async path => { events.push("delete:"+path); if(failDelete)throw new MikrotikError(403,path,"rejected") },
|
||||||
|
}
|
||||||
|
const persist=async()=>{events.push("persist");if(failPersist)throw Error("История недоступна")}
|
||||||
|
const reset=()=>{events=[];puts=0;failAt=0;badAck=false;failDelete=false;failPersist=false;rows=[owned,{".id":"*foreign",comment:"Keep default route"}]}
|
||||||
|
const payloads=[{"dst-address":"198.18.88.1/32"},{"dst-address":"198.18.88.2/32"}]
|
||||||
|
reset();assert.deepEqual(await executeRecursiveApply(client,payloads,persist),{pushed:2,deleted:1});assert.deepEqual(events,["put:1","put:2","delete:/ip/route/*old","persist"])
|
||||||
|
for(const invalid of [{},[owned,owned],[{...owned,static:undefined}],[{...owned,blackhole:"true"}]]){reset();rows=invalid;await assert.rejects(executeRecursiveApply(client,payloads,persist));assert.deepEqual(events,[])}
|
||||||
|
async function expectState(state:string,confirmed:number){await assert.rejects(executeRecursiveApply(client,payloads,persist),(e:unknown)=>{assert(e instanceof RecursiveApplyError);assert.equal(e.receipt.state,state);assert.equal(e.receipt.confirmedWrites,confirmed);assert.equal(e.receipt.metadataRecorded,false);return true})}
|
||||||
|
reset();failAt=1;rejection=new MikrotikError(401,"/ip/route","denied");await expectState("rejected",0);assert.deepEqual(events,["put:1"])
|
||||||
|
reset();failAt=2;rejection=new MikrotikError(403,"/ip/route","denied");await expectState("partial",1);assert.deepEqual(events,["put:1","put:2"])
|
||||||
|
reset();failAt=2;rejection=new Error("ACK второго PUT потерян");await expectState("unknown",1);assert.deepEqual(events,["put:1","put:2"])
|
||||||
|
reset();badAck=true;await expectState("unknown",0);assert.deepEqual(events,["put:1"])
|
||||||
|
reset();failDelete=true;await expectState("partial",2);assert.deepEqual(events,["put:1","put:2","delete:/ip/route/*old"])
|
||||||
|
reset();failPersist=true;await expectState("applied",3)
|
||||||
|
reset();rows=[];assert.deepEqual(await executeRecursiveApply(client,[],persist),{pushed:0,deleted:0});assert.deepEqual(events,["persist"])
|
||||||
|
console.log("Recursive apply: strict ownership before PUT, foreign retention, partial/unknown/rejected/applied, no retry/delete after rejection")
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { MikrotikClient } from "./mikrotik.js"
|
||||||
|
import { MikrotikError } from "./mikrotik.js"
|
||||||
|
import { parseRecursiveCatalog } from "./recursive-route-catalog.js"
|
||||||
|
|
||||||
|
export class RecursiveApplyError extends Error {
|
||||||
|
constructor(message: string, public readonly receipt: {
|
||||||
|
state: "partial" | "unknown" | "rejected" | "applied"
|
||||||
|
confirmedWrites: number
|
||||||
|
createdIds: string[]
|
||||||
|
deletedIds: string[]
|
||||||
|
needsReview: boolean
|
||||||
|
metadataRecorded: boolean
|
||||||
|
}) { super(message); this.name = "RecursiveApplyError" }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeRecursiveApply(
|
||||||
|
client: Pick<MikrotikClient, "get" | "put" | "delete">,
|
||||||
|
payloads: Record<string, string>[],
|
||||||
|
persist: () => Promise<void>,
|
||||||
|
) {
|
||||||
|
// The editor replaces every owned row: reject unsupported ownership before any write.
|
||||||
|
const existing = parseRecursiveCatalog(await client.get<unknown>("/ip/route"))
|
||||||
|
const createdIds: string[] = [], deletedIds: string[] = []
|
||||||
|
let nativeComplete = false
|
||||||
|
try {
|
||||||
|
for (const payload of payloads) {
|
||||||
|
const result = await client.put<unknown>("/ip/route", payload)
|
||||||
|
if (!result || typeof result !== "object" || !(".id" in result) || typeof result[".id"] !== "string" || !result[".id"].trim()) throw new Error("RouterOS не подтвердил ID созданного маршрута")
|
||||||
|
createdIds.push(result[".id"])
|
||||||
|
}
|
||||||
|
for (const route of existing) {
|
||||||
|
await client.delete("/ip/route/" + encodeURIComponent(route.id))
|
||||||
|
deletedIds.push(route.id)
|
||||||
|
}
|
||||||
|
nativeComplete = true
|
||||||
|
await persist()
|
||||||
|
return { pushed: createdIds.length, deleted: deletedIds.length }
|
||||||
|
} catch (error) {
|
||||||
|
const rejected = error instanceof MikrotikError && error.statusCode >= 400 && error.statusCode < 500
|
||||||
|
const confirmedWrites = createdIds.length + deletedIds.length
|
||||||
|
const state = nativeComplete ? "applied" : rejected ? (confirmedWrites ? "partial" : "rejected") : "unknown"
|
||||||
|
throw new RecursiveApplyError(`Подтверждено записей RouterOS: ${confirmedWrites}. Обновите каталог перед повторной попыткой. ${String(error)}`, {
|
||||||
|
state, confirmedWrites, createdIds, deletedIds, needsReview: state !== "rejected", metadataRecorded: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { isIP } from "node:net"
|
||||||
|
const destination = z.string().trim().refine(value => {
|
||||||
|
const [address, prefix, extra] = value.split("/")
|
||||||
|
return extra === undefined && isIP(address ?? "") === 4 && (prefix === undefined || /^\d+$/.test(prefix) && Number(prefix) <= 32)
|
||||||
|
}, "Нужен IPv4 адрес или префикс")
|
||||||
|
const gateway = z.string().trim().refine(value => {
|
||||||
|
const [address, iface, extra] = value.split("%")
|
||||||
|
return extra === undefined && isIP(address ?? "") === 4 && (iface === undefined || Boolean(iface) && !/\s/.test(iface))
|
||||||
|
}, "Нужен IPv4 шлюз с необязательным интерфейсом")
|
||||||
const serverId = z.union([z.number().int().positive(), z.string().trim().regex(/^\d+$/)])
|
const serverId = z.union([z.number().int().positive(), z.string().trim().regex(/^\d+$/)])
|
||||||
.transform(Number).refine(value => Number.isSafeInteger(value) && value > 0)
|
.transform(Number).refine(value => Number.isSafeInteger(value) && value > 0)
|
||||||
export const recursiveTargetSchema = z.object({ serverId })
|
export const recursiveTargetSchema = z.object({ serverId })
|
||||||
export const recursiveInputRowSchema = z.object({
|
export const recursiveInputRowSchema = z.object({
|
||||||
id: z.string().optional().default(""),
|
id: z.string().optional().default(""),
|
||||||
dstAddress: z.string().trim().min(1), gateway: z.string().trim().min(1),
|
dstAddress: destination, gateway,
|
||||||
distance: z.number().int().min(0).max(255),
|
distance: z.number().int().min(0).max(255),
|
||||||
scope: z.number().int().min(0).max(255).nullable(),
|
scope: z.number().int().min(0).max(255).nullable(),
|
||||||
targetScope: z.number().int().min(0).max(255).nullable(),
|
targetScope: z.number().int().min(0).max(255).nullable(),
|
||||||
routingTable: z.string().trim().min(1), checkGateway: z.string(),
|
routingTable: z.string().trim().min(1), checkGateway: z.enum(["none", "arp", "ping", "bfd"]),
|
||||||
country: z.string(), comment: z.string(), disabled: z.boolean(),
|
country: z.string(), comment: z.string(), disabled: z.boolean(),
|
||||||
})
|
})
|
||||||
export const recursiveInputRowsSchema = z.array(recursiveInputRowSchema)
|
export const recursiveInputRowsSchema = z.array(recursiveInputRowSchema)
|
||||||
|
|||||||
@@ -749,3 +749,35 @@ POST optimize при отказе записи возвращает502 и reques
|
|||||||
**Runtime.** Backend ux-optimizer87c/version1.0.0-test.optimizer87c, image sha256:22cc9a09a89038e58c15dfd6224191d1fab7ad63f7af6f80ecc9e7577483d390; frontend ux-optimizer87/version1.0.0-test.optimizer87, image sha256:22f05c341a7dc0e40e04f23b0da61ba5cfbf31d741d4e22b0c9bcd71c85e0c49. Backend/PG healthy, frontend served/browser passed. Backend entry/node --check/pg_restore18.6, frontend entry/node --check и compose --wait passed. Три backend builds87/87b/87c по двум реальным flag incompatibilities; frontend build один, apt cache сохранён. Context backend routing83 +2 routes, затем4 файлов final-c; override SHA c04289ab57294d57cc9aed292e247e144c171ade3d1101f6ebb84129e41b065be; frontend ipsec77 +OSPF page, override dab57aad2cb248553b0fee9da73069f7a31cb627b26c414f03320315a299756db. Labels не Git SHA; это рабочие файлы до локального коммита. .env backup приватный, миграций нет. Один локальный commit без push.
|
**Runtime.** Backend ux-optimizer87c/version1.0.0-test.optimizer87c, image sha256:22cc9a09a89038e58c15dfd6224191d1fab7ad63f7af6f80ecc9e7577483d390; frontend ux-optimizer87/version1.0.0-test.optimizer87, image sha256:22f05c341a7dc0e40e04f23b0da61ba5cfbf31d741d4e22b0c9bcd71c85e0c49. Backend/PG healthy, frontend served/browser passed. Backend entry/node --check/pg_restore18.6, frontend entry/node --check и compose --wait passed. Три backend builds87/87b/87c по двум реальным flag incompatibilities; frontend build один, apt cache сохранён. Context backend routing83 +2 routes, затем4 файлов final-c; override SHA c04289ab57294d57cc9aed292e247e144c171ade3d1101f6ebb84129e41b065be; frontend ipsec77 +OSPF page, override dab57aad2cb248553b0fee9da73069f7a31cb627b26c414f03320315a299756db. Labels не Git SHA; это рабочие файлы до локального коммита. .env backup приватный, миграций нет. Один локальный commit без push.
|
||||||
|
|
||||||
Продолжение88 — recursive routes, затем диагностика. BFD sustained/alternate-path failover, IPv6/scale/maximum throughput, Portal SSO/browser auth и другие внешние ограничения сохраняются; lab AUTH_REQUIRED=false, RBAC checked отдельно. API /get и .proplist/query подтверждены [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI) и реальным устройством. Квота23% использовано, этап12.5 открыт.
|
Продолжение88 — recursive routes, затем диагностика. BFD sustained/alternate-path failover, IPv6/scale/maximum throughput, Portal SSO/browser auth и другие внешние ограничения сохраняются; lab AUTH_REQUIRED=false, RBAC checked отдельно. API /get и .proplist/query подтверждены [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI) и реальным устройством. Квота23% использовано, этап12.5 открыт.
|
||||||
|
|
||||||
|
## 12.5.88 — рекурсивные маршруты: реальные операции и история (2026-10-09)
|
||||||
|
|
||||||
|
**Исправления.** Все6 путей catalog/gateways/catalog/legacy reads/PUT/restore проверяют enabled=false и возвращают409 до опроса/применения любого типа устройства. История остаётся доступным чтением БД. Создание RouterOS route исправлено с POST без add на PUT. Весь управляемый набор проходит strict parse до записей: неподдерживаемая owned row не исключается молча перед удалением. IPv4 destination/gateway и check-gateway проверяются до native writes. Замена кэша выполняется в транзакции вместо независимых delete/insert.
|
||||||
|
|
||||||
|
Request-local receipt сохраняет подтверждённые createdIds/deletedIds и число записей. RouterOS4xx после успешных шагов → partial; первый4xx → rejected; потерянный ACK/неправильный ответ/transport → unknown; ошибка кэша/истории после native completion → applied/metadataRecorded=false. После успешного apply, но отказа заключительного чтения кэша → applied/metadataRecorded=true. Ошибки операций возвращают502, последующие записи не выполняются, automatic retry/rollback не добавлены. Старые/default/foreign routes не удаляются при отказе создания новых; полная distributed transaction/idempotency не заявляется.
|
||||||
|
|
||||||
|
| Проверка | Подтверждённый результат |
|
||||||
|
| --- | --- |
|
||||||
|
| Disabled Home Router и Exit Node | По6 запросов409; контрольный HTTP probe0 |
|
||||||
|
| Native PUT + strict catalog | Формат/ID/flags подтверждены на двух CHR отдельным малым probe |
|
||||||
|
| Recursive resolution | gateway198.18.189.peer → resolver host route → immediate-gw198.18.88.peer%ownGRE |
|
||||||
|
| Data plane | Own loopback Ping3/3 в обе стороны в окончательном run |
|
||||||
|
| API/history | Apply, изменение distance/disabled, restore своей версии; invalid CIDR400 без записи |
|
||||||
|
| Fault injection | partial1/unknown1/applied2 подтверждённых записей; независимый native GET |
|
||||||
|
| Browser | 4/4 ПК/320px, две реальные формы добавления; reload context, pageerror0 |
|
||||||
|
|
||||||
|
**Стенд.** Свои GRE и link198.18.88.0/30, bridge loopbacks198.18.188.1/32↔2/32 и probe IP198.18.189.1/32↔2/32. Неуправляемый собственный resolver /32 имеет scope10; управляемый remote loopback /32, gateway probe IP, scope30/target-scope11/distance150/check-gateway none. Native immediate-gw доказал двухступенчатое разрешение через GRE, Ping подтвердил передачу. Это проверка recursion/data plane, не loss-based failover или SLA. Default/public/metadata routes и195.195.195.195/32 сохранены.
|
||||||
|
|
||||||
|
**Доказательства.** [Native/API/DB отчёт](ux-ui/acceptance/recursive-routes-2026-10-09.json). Production API применил конфигурацию обоих устройств, прочитал каталог, изменил distance/disabled и восстановил только созданную в этом run версию. Revisions baseline≤35 проверен, чтобы тестовые записи не вызвали pruning старой истории. HTTP409 проверены на двух собственных disabled DB rows с probe на private Docker DNS; fake rows и их revisions удалены каскадно.
|
||||||
|
|
||||||
|
Ошибки инъецированы в тот же native executor: второй PUT отклонён до передачи → partial и сохранён старый маршрут; второй PUT выполнен, ACK заменён исключением → unknown, фактические3 owned rows прочитаны отдельно; persist callback отказал после replace → applied, новый набор уже на RouterOS. API-обёртка этих receipts и поздний getRoutes отказ проверены HTTP/unit. Реальный отказ PostgreSQL/физический TCP disconnect не создавался.
|
||||||
|
|
||||||
|
**Браузер.** Настоящий backend/frontend без mocks: два устройства×1280/320. Раскрытый endpoint и URL/reload, document fits, затем на320px заполнена и отправлена форма: gateway probe IP/scope30/target11/none/distance152, новый private prefix198.18.188.250/32. API подтвердил два маршрута, sheet закрылась, две группы видны. [Browser отчёт](ux-ui/acceptance/browser-recursive-2026-10-09.json), [ПК](ux-ui/screens/recursive-real-server1-1280-2026-10-09.png), [320px форма](ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png) просмотрена. Общие Data Grid Filtering2/Sheet8/Frame из плана сохранены, frontend source не менялся.
|
||||||
|
|
||||||
|
**Проверки/оснастка.** 29 профильных E2E data/values/links passed. Backend recursive HTTP/catalog/executor tests, TypeScript, scoped ESLint/diff passed. Проверены invalid before writes, foreign retention, create/delete ordering, unsupported owned preflight, malformed ACK, rejected/partial/unknown/applied, поздняя ошибка чтения и disabled обоих типов. Первый native run Ping0/3: обратный маршрут ещё не создан; порядок исправлен на предварительное применение обеих сторон. Второй начальный Ping2/3: ограниченное ожидание готовности пути добавлено, до3 коротких попыток с сохранением результатов. Финальный run получил3/3 с первой попытки на обоих CHR. Ошибки оснастки не считаются приёмкой; оба cleanup passed.
|
||||||
|
|
||||||
|
**Очистка.** Native managed routes сверены по уникальному name-prefix, остальные свои GRE/bridge/IP/resolver по exact ID/атрибутам. Свои объекты удалены, baseline IDs4 коллекций×2 совпали. Исходно пустой recursive cache восстановлен; удалены только новые собственные revisions после проверки payload/comment и ID, прежние revision IDs сохранены. Последовательности PG увеличились из-за тестовых insert; это не побитовый restore БД. Оба CHR доступны, credentials неизменны, fixture containers удалены.
|
||||||
|
|
||||||
|
**Runtime/фиксация.** Одна backend сборка ux-recursive88/version1.0.0-test.recursive88, image sha256:d358ec1d6ea87ea7f29a12cc4fd3c1a6a7929b854bcdc732d956c702fe4dc587. Backend/PG healthy, frontend ux-optimizer87 сохранён и принят браузером. Context optimizer87 final-c +6 backend файлов, override SHA de4caf2aced38ba7e490f6a459a19d359cc92cb5b098564b799c05bc86be1365; label не Git SHA, рабочее состояние до коммита. Entry/node --check/pg_restore18.6, compose --wait passed, apt cached. .env backup приватный, миграций нет. Один локальный commit без push. REST метод подтверждён [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI).
|
||||||
|
|
||||||
|
Следующий89 — диагностика/терминал: реальный запуск, ограниченные команды, ошибки, права и контекст. Routing alternate-path/BFD sustained failover, IPv6/scale/physical disconnect/durable retry/Portal SSO и остальные внешние границы остаются открытыми. Квота38% использовано после блока; этап12.5 открыт.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# План реализации UX/UI по пятичасовым окнам
|
# План реализации UX/UI по пятичасовым окнам
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-09: 12.5.87 принял production OSPF optimizer preview/apply/repeat на двух своих GRE парах, partial/unknown с проверкой native costs и4 live UI проверки1280/320. Исправлены права кнопки и чтение эффективного disabled OSPF templates/instances/areas. Baseline7 коллекций×2 восстановлен. Backend ux-optimizer87c/frontend ux-optimizer87, healthy/served. Следующий12.5.88 — рекурсивные маршруты: запрет опроса выключенных устройств, реальные чтение/применение/история и достоверность поздних ошибок; далее диагностика. Квота23% использовано после блока, этап12.5 открыт.
|
Актуальное продолжение 2026-10-09: 12.5.88 завершил рекурсивные маршруты: disabled guard всех6 native путей, PUT/строгий preflight/кэш transaction/partial-unknown-applied receipts. Реальный recursive gateway через свой GRE, Ping3/3×2, API apply/restore,12 disabled409/probe0 и4 live UI cases с2 добавлениями на320px. Native baseline4 коллекций×2 и прежние cache/history восстановлены. Backend ux-recursive88/frontend ux-optimizer87. Следующий12.5.89 — реальная диагностика и терминал, права/ошибки/контекст; затем оставшиеся routing failover и внешние границы. Квота38% использовано; этап12.5 открыт.
|
||||||
|
|
||||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-09. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-09. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||||
|
|
||||||
|
|||||||
+34
-2
@@ -1,6 +1,6 @@
|
|||||||
# UX/UI: журнал реализации
|
# UX/UI: журнал реализации
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-09: 12.5.87 принял production OSPF optimizer preview/apply/repeat на двух своих GRE парах, partial/unknown с проверкой native costs и4 live UI проверки1280/320. Исправлены права кнопки и чтение эффективного disabled OSPF templates/instances/areas. Baseline7 коллекций×2 восстановлен. Backend ux-optimizer87c/frontend ux-optimizer87, healthy/served. Следующий12.5.88 — рекурсивные маршруты: запрет опроса выключенных устройств, реальные чтение/применение/история и достоверность поздних ошибок; далее диагностика. Квота23% использовано после блока, этап12.5 открыт.
|
Актуальное продолжение 2026-10-09: 12.5.88 завершил рекурсивные маршруты: disabled guard всех6 native путей, PUT/строгий preflight/кэш transaction/partial-unknown-applied receipts. Реальный recursive gateway через свой GRE, Ping3/3×2, API apply/restore,12 disabled409/probe0 и4 live UI cases с2 добавлениями на320px. Native baseline4 коллекций×2 и прежние cache/history восстановлены. Backend ux-recursive88/frontend ux-optimizer87. Следующий12.5.89 — реальная диагностика и терминал, права/ошибки/контекст; затем оставшиеся routing failover и внешние границы. Квота38% использовано; этап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).
|
||||||
|
|
||||||
@@ -36,7 +36,39 @@
|
|||||||
| 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–87 и ограничения ниже. Runtime backend ux-optimizer87c/frontend ux-optimizer87. |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–88 и ограничения ниже. Runtime backend ux-recursive88/frontend ux-optimizer87. |
|
||||||
|
|
||||||
|
## 12.5.88 — рекурсивные маршруты: реальные операции и история (2026-10-09)
|
||||||
|
|
||||||
|
**Исправления.** Все6 путей catalog/gateways/catalog/legacy reads/PUT/restore проверяют enabled=false и возвращают409 до опроса/применения любого типа устройства. История остаётся доступным чтением БД. Создание RouterOS route исправлено с POST без add на PUT. Весь управляемый набор проходит strict parse до записей: неподдерживаемая owned row не исключается молча перед удалением. IPv4 destination/gateway и check-gateway проверяются до native writes. Замена кэша выполняется в транзакции вместо независимых delete/insert.
|
||||||
|
|
||||||
|
Request-local receipt сохраняет подтверждённые createdIds/deletedIds и число записей. RouterOS4xx после успешных шагов → partial; первый4xx → rejected; потерянный ACK/неправильный ответ/transport → unknown; ошибка кэша/истории после native completion → applied/metadataRecorded=false. После успешного apply, но отказа заключительного чтения кэша → applied/metadataRecorded=true. Ошибки операций возвращают502, последующие записи не выполняются, automatic retry/rollback не добавлены. Старые/default/foreign routes не удаляются при отказе создания новых; полная distributed transaction/idempotency не заявляется.
|
||||||
|
|
||||||
|
| Проверка | Подтверждённый результат |
|
||||||
|
| --- | --- |
|
||||||
|
| Disabled Home Router и Exit Node | По6 запросов409; контрольный HTTP probe0 |
|
||||||
|
| Native PUT + strict catalog | Формат/ID/flags подтверждены на двух CHR отдельным малым probe |
|
||||||
|
| Recursive resolution | gateway198.18.189.peer → resolver host route → immediate-gw198.18.88.peer%ownGRE |
|
||||||
|
| Data plane | Own loopback Ping3/3 в обе стороны в окончательном run |
|
||||||
|
| API/history | Apply, изменение distance/disabled, restore своей версии; invalid CIDR400 без записи |
|
||||||
|
| Fault injection | partial1/unknown1/applied2 подтверждённых записей; независимый native GET |
|
||||||
|
| Browser | 4/4 ПК/320px, две реальные формы добавления; reload context, pageerror0 |
|
||||||
|
|
||||||
|
**Стенд.** Свои GRE и link198.18.88.0/30, bridge loopbacks198.18.188.1/32↔2/32 и probe IP198.18.189.1/32↔2/32. Неуправляемый собственный resolver /32 имеет scope10; управляемый remote loopback /32, gateway probe IP, scope30/target-scope11/distance150/check-gateway none. Native immediate-gw доказал двухступенчатое разрешение через GRE, Ping подтвердил передачу. Это проверка recursion/data plane, не loss-based failover или SLA. Default/public/metadata routes и195.195.195.195/32 сохранены.
|
||||||
|
|
||||||
|
**Доказательства.** [Native/API/DB отчёт](ux-ui/acceptance/recursive-routes-2026-10-09.json). Production API применил конфигурацию обоих устройств, прочитал каталог, изменил distance/disabled и восстановил только созданную в этом run версию. Revisions baseline≤35 проверен, чтобы тестовые записи не вызвали pruning старой истории. HTTP409 проверены на двух собственных disabled DB rows с probe на private Docker DNS; fake rows и их revisions удалены каскадно.
|
||||||
|
|
||||||
|
Ошибки инъецированы в тот же native executor: второй PUT отклонён до передачи → partial и сохранён старый маршрут; второй PUT выполнен, ACK заменён исключением → unknown, фактические3 owned rows прочитаны отдельно; persist callback отказал после replace → applied, новый набор уже на RouterOS. API-обёртка этих receipts и поздний getRoutes отказ проверены HTTP/unit. Реальный отказ PostgreSQL/физический TCP disconnect не создавался.
|
||||||
|
|
||||||
|
**Браузер.** Настоящий backend/frontend без mocks: два устройства×1280/320. Раскрытый endpoint и URL/reload, document fits, затем на320px заполнена и отправлена форма: gateway probe IP/scope30/target11/none/distance152, новый private prefix198.18.188.250/32. API подтвердил два маршрута, sheet закрылась, две группы видны. [Browser отчёт](ux-ui/acceptance/browser-recursive-2026-10-09.json), [ПК](ux-ui/screens/recursive-real-server1-1280-2026-10-09.png), [320px форма](ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png) просмотрена. Общие Data Grid Filtering2/Sheet8/Frame из плана сохранены, frontend source не менялся.
|
||||||
|
|
||||||
|
**Проверки/оснастка.** 29 профильных E2E data/values/links passed. Backend recursive HTTP/catalog/executor tests, TypeScript, scoped ESLint/diff passed. Проверены invalid before writes, foreign retention, create/delete ordering, unsupported owned preflight, malformed ACK, rejected/partial/unknown/applied, поздняя ошибка чтения и disabled обоих типов. Первый native run Ping0/3: обратный маршрут ещё не создан; порядок исправлен на предварительное применение обеих сторон. Второй начальный Ping2/3: ограниченное ожидание готовности пути добавлено, до3 коротких попыток с сохранением результатов. Финальный run получил3/3 с первой попытки на обоих CHR. Ошибки оснастки не считаются приёмкой; оба cleanup passed.
|
||||||
|
|
||||||
|
**Очистка.** Native managed routes сверены по уникальному name-prefix, остальные свои GRE/bridge/IP/resolver по exact ID/атрибутам. Свои объекты удалены, baseline IDs4 коллекций×2 совпали. Исходно пустой recursive cache восстановлен; удалены только новые собственные revisions после проверки payload/comment и ID, прежние revision IDs сохранены. Последовательности PG увеличились из-за тестовых insert; это не побитовый restore БД. Оба CHR доступны, credentials неизменны, fixture containers удалены.
|
||||||
|
|
||||||
|
**Runtime/фиксация.** Одна backend сборка ux-recursive88/version1.0.0-test.recursive88, image sha256:d358ec1d6ea87ea7f29a12cc4fd3c1a6a7929b854bcdc732d956c702fe4dc587. Backend/PG healthy, frontend ux-optimizer87 сохранён и принят браузером. Context optimizer87 final-c +6 backend файлов, override SHA de4caf2aced38ba7e490f6a459a19d359cc92cb5b098564b799c05bc86be1365; label не Git SHA, рабочее состояние до коммита. Entry/node --check/pg_restore18.6, compose --wait passed, apt cached. .env backup приватный, миграций нет. Один локальный commit без push. REST метод подтверждён [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI).
|
||||||
|
|
||||||
|
Следующий89 — диагностика/терминал: реальный запуск, ограниченные команды, ошибки, права и контекст. Routing alternate-path/BFD sustained failover, IPv6/scale/physical disconnect/durable retry/Portal SSO и остальные внешние границы остаются открытыми. Квота38% использовано после блока; этап12.5 открыт.
|
||||||
|
|
||||||
## 12.5.87 — OSPF optimizer: права, реальные costs и частичный результат (2026-10-09)
|
## 12.5.87 — OSPF optimizer: права, реальные costs и частичный результат (2026-10-09)
|
||||||
|
|
||||||
|
|||||||
@@ -2058,3 +2058,35 @@ POST optimize при отказе записи возвращает502 и reques
|
|||||||
**Runtime.** Backend ux-optimizer87c/version1.0.0-test.optimizer87c, image sha256:22cc9a09a89038e58c15dfd6224191d1fab7ad63f7af6f80ecc9e7577483d390; frontend ux-optimizer87/version1.0.0-test.optimizer87, image sha256:22f05c341a7dc0e40e04f23b0da61ba5cfbf31d741d4e22b0c9bcd71c85e0c49. Backend/PG healthy, frontend served/browser passed. Backend entry/node --check/pg_restore18.6, frontend entry/node --check и compose --wait passed. Три backend builds87/87b/87c по двум реальным flag incompatibilities; frontend build один, apt cache сохранён. Context backend routing83 +2 routes, затем4 файлов final-c; override SHA c04289ab57294d57cc9aed292e247e144c171ade3d1101f6ebb84129e41b065be; frontend ipsec77 +OSPF page, override dab57aad2cb248553b0fee9da73069f7a31cb627b26c414f03320315a299756db. Labels не Git SHA; это рабочие файлы до локального коммита. .env backup приватный, миграций нет. Один локальный commit без push.
|
**Runtime.** Backend ux-optimizer87c/version1.0.0-test.optimizer87c, image sha256:22cc9a09a89038e58c15dfd6224191d1fab7ad63f7af6f80ecc9e7577483d390; frontend ux-optimizer87/version1.0.0-test.optimizer87, image sha256:22f05c341a7dc0e40e04f23b0da61ba5cfbf31d741d4e22b0c9bcd71c85e0c49. Backend/PG healthy, frontend served/browser passed. Backend entry/node --check/pg_restore18.6, frontend entry/node --check и compose --wait passed. Три backend builds87/87b/87c по двум реальным flag incompatibilities; frontend build один, apt cache сохранён. Context backend routing83 +2 routes, затем4 файлов final-c; override SHA c04289ab57294d57cc9aed292e247e144c171ade3d1101f6ebb84129e41b065be; frontend ipsec77 +OSPF page, override dab57aad2cb248553b0fee9da73069f7a31cb627b26c414f03320315a299756db. Labels не Git SHA; это рабочие файлы до локального коммита. .env backup приватный, миграций нет. Один локальный commit без push.
|
||||||
|
|
||||||
Продолжение88 — recursive routes, затем диагностика. BFD sustained/alternate-path failover, IPv6/scale/maximum throughput, Portal SSO/browser auth и другие внешние ограничения сохраняются; lab AUTH_REQUIRED=false, RBAC checked отдельно. API /get и .proplist/query подтверждены [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI) и реальным устройством. Квота23% использовано, этап12.5 открыт.
|
Продолжение88 — recursive routes, затем диагностика. BFD sustained/alternate-path failover, IPv6/scale/maximum throughput, Portal SSO/browser auth и другие внешние ограничения сохраняются; lab AUTH_REQUIRED=false, RBAC checked отдельно. API /get и .proplist/query подтверждены [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI) и реальным устройством. Квота23% использовано, этап12.5 открыт.
|
||||||
|
|
||||||
|
## 12.5.88 — рекурсивные маршруты: реальные операции и история (2026-10-09)
|
||||||
|
|
||||||
|
**Исправления.** Все6 путей catalog/gateways/catalog/legacy reads/PUT/restore проверяют enabled=false и возвращают409 до опроса/применения любого типа устройства. История остаётся доступным чтением БД. Создание RouterOS route исправлено с POST без add на PUT. Весь управляемый набор проходит strict parse до записей: неподдерживаемая owned row не исключается молча перед удалением. IPv4 destination/gateway и check-gateway проверяются до native writes. Замена кэша выполняется в транзакции вместо независимых delete/insert.
|
||||||
|
|
||||||
|
Request-local receipt сохраняет подтверждённые createdIds/deletedIds и число записей. RouterOS4xx после успешных шагов → partial; первый4xx → rejected; потерянный ACK/неправильный ответ/transport → unknown; ошибка кэша/истории после native completion → applied/metadataRecorded=false. После успешного apply, но отказа заключительного чтения кэша → applied/metadataRecorded=true. Ошибки операций возвращают502, последующие записи не выполняются, automatic retry/rollback не добавлены. Старые/default/foreign routes не удаляются при отказе создания новых; полная distributed transaction/idempotency не заявляется.
|
||||||
|
|
||||||
|
| Проверка | Подтверждённый результат |
|
||||||
|
| --- | --- |
|
||||||
|
| Disabled Home Router и Exit Node | По6 запросов409; контрольный HTTP probe0 |
|
||||||
|
| Native PUT + strict catalog | Формат/ID/flags подтверждены на двух CHR отдельным малым probe |
|
||||||
|
| Recursive resolution | gateway198.18.189.peer → resolver host route → immediate-gw198.18.88.peer%ownGRE |
|
||||||
|
| Data plane | Own loopback Ping3/3 в обе стороны в окончательном run |
|
||||||
|
| API/history | Apply, изменение distance/disabled, restore своей версии; invalid CIDR400 без записи |
|
||||||
|
| Fault injection | partial1/unknown1/applied2 подтверждённых записей; независимый native GET |
|
||||||
|
| Browser | 4/4 ПК/320px, две реальные формы добавления; reload context, pageerror0 |
|
||||||
|
|
||||||
|
**Стенд.** Свои GRE и link198.18.88.0/30, bridge loopbacks198.18.188.1/32↔2/32 и probe IP198.18.189.1/32↔2/32. Неуправляемый собственный resolver /32 имеет scope10; управляемый remote loopback /32, gateway probe IP, scope30/target-scope11/distance150/check-gateway none. Native immediate-gw доказал двухступенчатое разрешение через GRE, Ping подтвердил передачу. Это проверка recursion/data plane, не loss-based failover или SLA. Default/public/metadata routes и195.195.195.195/32 сохранены.
|
||||||
|
|
||||||
|
**Доказательства.** [Native/API/DB отчёт](ux-ui/acceptance/recursive-routes-2026-10-09.json). Production API применил конфигурацию обоих устройств, прочитал каталог, изменил distance/disabled и восстановил только созданную в этом run версию. Revisions baseline≤35 проверен, чтобы тестовые записи не вызвали pruning старой истории. HTTP409 проверены на двух собственных disabled DB rows с probe на private Docker DNS; fake rows и их revisions удалены каскадно.
|
||||||
|
|
||||||
|
Ошибки инъецированы в тот же native executor: второй PUT отклонён до передачи → partial и сохранён старый маршрут; второй PUT выполнен, ACK заменён исключением → unknown, фактические3 owned rows прочитаны отдельно; persist callback отказал после replace → applied, новый набор уже на RouterOS. API-обёртка этих receipts и поздний getRoutes отказ проверены HTTP/unit. Реальный отказ PostgreSQL/физический TCP disconnect не создавался.
|
||||||
|
|
||||||
|
**Браузер.** Настоящий backend/frontend без mocks: два устройства×1280/320. Раскрытый endpoint и URL/reload, document fits, затем на320px заполнена и отправлена форма: gateway probe IP/scope30/target11/none/distance152, новый private prefix198.18.188.250/32. API подтвердил два маршрута, sheet закрылась, две группы видны. [Browser отчёт](ux-ui/acceptance/browser-recursive-2026-10-09.json), [ПК](ux-ui/screens/recursive-real-server1-1280-2026-10-09.png), [320px форма](ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png) просмотрена. Общие Data Grid Filtering2/Sheet8/Frame из плана сохранены, frontend source не менялся.
|
||||||
|
|
||||||
|
**Проверки/оснастка.** 29 профильных E2E data/values/links passed. Backend recursive HTTP/catalog/executor tests, TypeScript, scoped ESLint/diff passed. Проверены invalid before writes, foreign retention, create/delete ordering, unsupported owned preflight, malformed ACK, rejected/partial/unknown/applied, поздняя ошибка чтения и disabled обоих типов. Первый native run Ping0/3: обратный маршрут ещё не создан; порядок исправлен на предварительное применение обеих сторон. Второй начальный Ping2/3: ограниченное ожидание готовности пути добавлено, до3 коротких попыток с сохранением результатов. Финальный run получил3/3 с первой попытки на обоих CHR. Ошибки оснастки не считаются приёмкой; оба cleanup passed.
|
||||||
|
|
||||||
|
**Очистка.** Native managed routes сверены по уникальному name-prefix, остальные свои GRE/bridge/IP/resolver по exact ID/атрибутам. Свои объекты удалены, baseline IDs4 коллекций×2 совпали. Исходно пустой recursive cache восстановлен; удалены только новые собственные revisions после проверки payload/comment и ID, прежние revision IDs сохранены. Последовательности PG увеличились из-за тестовых insert; это не побитовый restore БД. Оба CHR доступны, credentials неизменны, fixture containers удалены.
|
||||||
|
|
||||||
|
**Runtime/фиксация.** Одна backend сборка ux-recursive88/version1.0.0-test.recursive88, image sha256:d358ec1d6ea87ea7f29a12cc4fd3c1a6a7929b854bcdc732d956c702fe4dc587. Backend/PG healthy, frontend ux-optimizer87 сохранён и принят браузером. Context optimizer87 final-c +6 backend файлов, override SHA de4caf2aced38ba7e490f6a459a19d359cc92cb5b098564b799c05bc86be1365; label не Git SHA, рабочее состояние до коммита. Entry/node --check/pg_restore18.6, compose --wait passed, apt cached. .env backup приватный, миграций нет. Один локальный commit без push. REST метод подтверждён [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI).
|
||||||
|
|
||||||
|
Следующий89 — диагностика/терминал: реальный запуск, ограниченные команды, ошибки, права и контекст. Routing alternate-path/BFD sustained failover, IPv6/scale/physical disconnect/durable retry/Portal SSO и остальные внешние границы остаются открытыми. Квота38% использовано после блока; этап12.5 открыт.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Тестовый сервер UX/UI — 2026-10-09
|
# Тестовый сервер UX/UI — 2026-10-09
|
||||||
|
|
||||||
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-optimizer87c/version1.0.0-test.optimizer87c, frontend ux-optimizer87/version1.0.0-test.optimizer87; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
|
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-recursive88/version1.0.0-test.recursive88, frontend ux-optimizer87/version1.0.0-test.optimizer87; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
|
||||||
|
|
||||||
| Параметр | Проверенное состояние |
|
| Параметр | Проверенное состояние |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -713,3 +713,5 @@ flowchart LR
|
|||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
|
|
||||||
12.5.87 (2026-10-09): OSPF optimizer native/API и4 browser checks passed, собственные объекты очищены, baseline7 коллекций×2 сохранён. Подробности/runtime происхождение — в журнале.
|
12.5.87 (2026-10-09): OSPF optimizer native/API и4 browser checks passed, собственные объекты очищены, baseline7 коллекций×2 сохранён. Подробности/runtime происхождение — в журнале.
|
||||||
|
|
||||||
|
12.5.88 (2026-10-09): recursion/GRE/Ping, apply/restore и4 browser checks passed; native baseline4 коллекций×2, cache/history сохранены. Backend recursive88, frontend optimizer87.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-09",
|
||||||
|
"phase": "2 320",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"width": 1280,
|
||||||
|
"recursiveRouteVisible": true,
|
||||||
|
"urlRestore": true,
|
||||||
|
"documentFits": true,
|
||||||
|
"realBrowserAddReceipt": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"width": 320,
|
||||||
|
"recursiveRouteVisible": true,
|
||||||
|
"urlRestore": true,
|
||||||
|
"documentFits": true,
|
||||||
|
"realBrowserAddReceipt": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 2,
|
||||||
|
"width": 1280,
|
||||||
|
"recursiveRouteVisible": true,
|
||||||
|
"urlRestore": true,
|
||||||
|
"documentFits": true,
|
||||||
|
"realBrowserAddReceipt": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 2,
|
||||||
|
"width": 320,
|
||||||
|
"recursiveRouteVisible": true,
|
||||||
|
"urlRestore": true,
|
||||||
|
"documentFits": true,
|
||||||
|
"realBrowserAddReceipt": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-09",
|
||||||
|
"name": "mm-rec88-1791554488522",
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"applied": {
|
||||||
|
"ok": true,
|
||||||
|
"serverId": 1,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": "11",
|
||||||
|
"dstAddress": "198.18.188.2/32",
|
||||||
|
"gateway": "198.18.189.2",
|
||||||
|
"distance": 150,
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 11,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-1791554488522",
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pushedRoutes": 1
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"serverId": 1,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": "*80000014",
|
||||||
|
"dstAddress": "198.18.188.2/32",
|
||||||
|
"gateway": "198.18.189.2",
|
||||||
|
"distance": 150,
|
||||||
|
"disabled": false,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 11,
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-1791554488522"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"live": true,
|
||||||
|
"stale": false
|
||||||
|
},
|
||||||
|
"recursiveNative": {
|
||||||
|
".id": "*80000014",
|
||||||
|
"active": "true",
|
||||||
|
"check-gateway": "none",
|
||||||
|
"comment": "MikrotikManager:recursive mm-rec88-1791554488522",
|
||||||
|
"disabled": "false",
|
||||||
|
"distance": "150",
|
||||||
|
"dst-address": "198.18.188.2/32",
|
||||||
|
"dynamic": "false",
|
||||||
|
"gateway": "198.18.189.2",
|
||||||
|
"immediate-gw": "198.18.88.2%mm-rec88-1791554488522",
|
||||||
|
"inactive": "false",
|
||||||
|
"routing-table": "main",
|
||||||
|
"scope": "30",
|
||||||
|
"static": "true",
|
||||||
|
"target-scope": "11"
|
||||||
|
},
|
||||||
|
"pingReceived": 3,
|
||||||
|
"historyRestore": true,
|
||||||
|
"invalidInputNoWrites": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 2,
|
||||||
|
"applied": {
|
||||||
|
"ok": true,
|
||||||
|
"serverId": 2,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": "18",
|
||||||
|
"dstAddress": "198.18.188.1/32",
|
||||||
|
"gateway": "198.18.189.1",
|
||||||
|
"distance": 150,
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 11,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-1791554488522",
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pushedRoutes": 1
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"serverId": 2,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": "*80000012",
|
||||||
|
"dstAddress": "198.18.188.1/32",
|
||||||
|
"gateway": "198.18.189.1",
|
||||||
|
"distance": 150,
|
||||||
|
"disabled": false,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 11,
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-1791554488522"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"live": true,
|
||||||
|
"stale": false
|
||||||
|
},
|
||||||
|
"recursiveNative": {
|
||||||
|
".id": "*80000012",
|
||||||
|
"active": "true",
|
||||||
|
"check-gateway": "none",
|
||||||
|
"comment": "MikrotikManager:recursive mm-rec88-1791554488522",
|
||||||
|
"disabled": "false",
|
||||||
|
"distance": "150",
|
||||||
|
"dst-address": "198.18.188.1/32",
|
||||||
|
"dynamic": "false",
|
||||||
|
"gateway": "198.18.189.1",
|
||||||
|
"immediate-gw": "198.18.88.1%mm-rec88-1791554488522",
|
||||||
|
"inactive": "false",
|
||||||
|
"routing-table": "main",
|
||||||
|
"scope": "30",
|
||||||
|
"static": "true",
|
||||||
|
"target-scope": "11"
|
||||||
|
},
|
||||||
|
"pingReceived": 3,
|
||||||
|
"historyRestore": true,
|
||||||
|
"invalidInputNoWrites": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disabled": [
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes/catalog",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes/gateways/catalog",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes/gateways",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes",
|
||||||
|
"method": "PUT",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "home-router",
|
||||||
|
"path": "/recursive-routes/revisions/7c8be412-5139-455a-a910-f61d20a9159c/restore",
|
||||||
|
"method": "POST",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes/catalog",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes/gateways/catalog",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes/gateways",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes",
|
||||||
|
"method": "PUT",
|
||||||
|
"status": 409
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "exit-node",
|
||||||
|
"path": "/recursive-routes/revisions/940ee856-a382-4713-b100-a548b70505c2/restore",
|
||||||
|
"method": "POST",
|
||||||
|
"status": 409
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"faults": [
|
||||||
|
{
|
||||||
|
"mode": "partial",
|
||||||
|
"receipt": {
|
||||||
|
"state": "partial",
|
||||||
|
"confirmedWrites": 1,
|
||||||
|
"createdIds": [
|
||||||
|
"*80000017"
|
||||||
|
],
|
||||||
|
"deletedIds": [],
|
||||||
|
"needsReview": true,
|
||||||
|
"metadataRecorded": false
|
||||||
|
},
|
||||||
|
"nativeIds": [
|
||||||
|
"*80000016",
|
||||||
|
"*80000017"
|
||||||
|
],
|
||||||
|
"nativeCount": 2,
|
||||||
|
"faultInjection": true,
|
||||||
|
"physicalDisconnect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "unknown",
|
||||||
|
"receipt": {
|
||||||
|
"state": "unknown",
|
||||||
|
"confirmedWrites": 1,
|
||||||
|
"createdIds": [
|
||||||
|
"*80000019"
|
||||||
|
],
|
||||||
|
"deletedIds": [],
|
||||||
|
"needsReview": true,
|
||||||
|
"metadataRecorded": false
|
||||||
|
},
|
||||||
|
"nativeIds": [
|
||||||
|
"*80000018",
|
||||||
|
"*80000019",
|
||||||
|
"*8000001A"
|
||||||
|
],
|
||||||
|
"nativeCount": 3,
|
||||||
|
"faultInjection": true,
|
||||||
|
"physicalDisconnect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "applied",
|
||||||
|
"receipt": {
|
||||||
|
"state": "applied",
|
||||||
|
"confirmedWrites": 2,
|
||||||
|
"createdIds": [
|
||||||
|
"*8000001C"
|
||||||
|
],
|
||||||
|
"deletedIds": [
|
||||||
|
"*8000001B"
|
||||||
|
],
|
||||||
|
"needsReview": true,
|
||||||
|
"metadataRecorded": false
|
||||||
|
},
|
||||||
|
"nativeIds": [
|
||||||
|
"*8000001C"
|
||||||
|
],
|
||||||
|
"nativeCount": 1,
|
||||||
|
"faultInjection": true,
|
||||||
|
"physicalDisconnect": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cleanup": [
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"dbCacheEmpty": true,
|
||||||
|
"historyBaseline": true,
|
||||||
|
"/interface/gre": true,
|
||||||
|
"/interface/bridge": true,
|
||||||
|
"/ip/address": true,
|
||||||
|
"/ip/route": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 2,
|
||||||
|
"dbCacheEmpty": true,
|
||||||
|
"historyBaseline": true,
|
||||||
|
"/interface/gre": true,
|
||||||
|
"/interface/bridge": true,
|
||||||
|
"/ip/address": true,
|
||||||
|
"/ip/route": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"probeRequests": 0,
|
||||||
|
"pingAttempts": {
|
||||||
|
"1": [
|
||||||
|
{
|
||||||
|
"received": 3,
|
||||||
|
"sent": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
{
|
||||||
|
"received": 3,
|
||||||
|
"sent": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nativeProbe": {
|
||||||
|
"date": "2026-10-09",
|
||||||
|
"out": [
|
||||||
|
{
|
||||||
|
"serverId": 1,
|
||||||
|
"raw": {
|
||||||
|
".id": "*8000000C",
|
||||||
|
"active": "true",
|
||||||
|
"check-gateway": "none",
|
||||||
|
"comment": "MikrotikManager:recursive mm-rec88-probe-1791553666907",
|
||||||
|
"disabled": "false",
|
||||||
|
"distance": "150",
|
||||||
|
"dst-address": "198.18.88.250/32",
|
||||||
|
"dynamic": "false",
|
||||||
|
"gateway": "194.226.121.1",
|
||||||
|
"immediate-gw": "194.226.121.1%ether1",
|
||||||
|
"inactive": "false",
|
||||||
|
"routing-table": "main",
|
||||||
|
"scope": "30",
|
||||||
|
"static": "true",
|
||||||
|
"target-scope": "10"
|
||||||
|
},
|
||||||
|
"mapped": {
|
||||||
|
"id": "*8000000C",
|
||||||
|
"dstAddress": "198.18.88.250/32",
|
||||||
|
"gateway": "194.226.121.1",
|
||||||
|
"distance": 150,
|
||||||
|
"disabled": false,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 10,
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-probe-1791553666907"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"serverId": 2,
|
||||||
|
"raw": {
|
||||||
|
".id": "*8000000C",
|
||||||
|
"active": "true",
|
||||||
|
"check-gateway": "none",
|
||||||
|
"comment": "MikrotikManager:recursive mm-rec88-probe-1791553667075",
|
||||||
|
"disabled": "false",
|
||||||
|
"distance": "150",
|
||||||
|
"dst-address": "198.18.88.250/32",
|
||||||
|
"dynamic": "false",
|
||||||
|
"gateway": "104.165.244.1",
|
||||||
|
"immediate-gw": "104.165.244.1%ether1",
|
||||||
|
"inactive": "false",
|
||||||
|
"routing-table": "main",
|
||||||
|
"scope": "30",
|
||||||
|
"static": "true",
|
||||||
|
"target-scope": "10"
|
||||||
|
},
|
||||||
|
"mapped": {
|
||||||
|
"id": "*8000000C",
|
||||||
|
"dstAddress": "198.18.88.250/32",
|
||||||
|
"gateway": "104.165.244.1",
|
||||||
|
"distance": 150,
|
||||||
|
"disabled": false,
|
||||||
|
"routingTable": "main",
|
||||||
|
"checkGateway": "none",
|
||||||
|
"scope": 30,
|
||||||
|
"targetScope": 10,
|
||||||
|
"country": "",
|
||||||
|
"comment": "mm-rec88-probe-1791553667075"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"initialAttempts": [
|
||||||
|
{
|
||||||
|
"result": "Ping0/3 before reverse route was configured",
|
||||||
|
"cleanupPassed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"result": "Initial Ping2/3 during path startup",
|
||||||
|
"cleanupPassed": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"runtime": {
|
||||||
|
"backend": "ux-recursive88",
|
||||||
|
"frontend": "ux-optimizer87",
|
||||||
|
"labelsAreGitSha": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Reference in new issue
Block a user