fix(routes): подтвердить применение и восстановление рекурсивных маршрутов

This commit is contained in:
Denozordec committed 2026-10-09 21:08:10 +07:00
1 parent 36c6a21ca5
commit 5b3133ce73
19 files changed
+659 -28

No files matched your search

+1 -1
View File
@@ -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: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: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: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",
+23 -3
View File
@@ -2,8 +2,10 @@ import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
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 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"
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
@@ -12,12 +14,13 @@ const app = Fastify()
app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
await app.register(createRecursiveRoutes({
getServer: async id => { lookup++; return id === 1 ? server : undefined },
getRoutes: async () => stored,
readRaw: async () => { if (rawFailure) throw new Error("Тест опроса"); return rawPayload },
getRoutes: async () => { if (lateReadFailure) throw Error("Сбой чтения после применения"); return stored },
readRaw: async () => { rawReads++; if (rawFailure) throw new Error("Тест опроса"); return rawPayload },
saveObserved: async (_id, rows) => { savedSnapshots++; stored = rows as typeof row[] },
apply: async (device, rows, source) => {
assert.equal(device.id, 1); writes++; applied.push({ rows: rows as typeof row[], source })
if (fail) throw new Error("Тест частичного применения")
if (applyError) throw applyError
stored = rows as typeof row[]; return { pushed: rows.length, deleted: 0 }
},
getRevision: async () => {
@@ -32,6 +35,7 @@ const invalid = [
{ 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, 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) {
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})
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")
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()
console.log("recursive routes HTTP: invalid/explicit-empty/zero/copy/error/restore ownership and payload passed without DB/RouterOS")
+28 -18
View File
@@ -1,5 +1,6 @@
import { recursiveApplyBodySchema, recursiveTargetSchema, recursiveInputRowsSchema } from '../services/recursive-route-validation.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 { z } from "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 {
mapRosManagedRoutes,
planRecursiveApply,
userRecursiveComment,
} from "../services/config-apply-plan.js"
import {
@@ -123,10 +123,11 @@ function toRouterPayload(route: RecursiveRouteDto): Record<string, string> {
}
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
const now = new Date().toISOString()
await db.insert(recursiveRoutes).values(
await tx.insert(recursiveRoutes).values(
routes.map((r, i) => ({
serverId,
sortOrder: i,
@@ -144,6 +145,7 @@ async function replaceDbRoutes(serverId: number, routes: RecursiveRouteDto[]) {
updatedAt: now,
})),
)
})
}
async function applyRecursiveToServer(
@@ -152,16 +154,7 @@ async function applyRecursiveToServer(
source: ConfigRevisionSource,
): Promise<{ pushed: number; deleted: number }> {
const client = MikrotikClient.fromServer(server)
const existing = await client.get<RosRoute[]>("/ip/route")
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)}`)
}
return executeRecursiveApply(client, routes.map(toRouterPayload), async () => {
await replaceDbRoutes(server.id, routes)
await appendRevisionIfChanged({
serverId: server.id,
@@ -169,11 +162,18 @@ async function applyRecursiveToServer(
source,
payload: canonicalRecursiveRoutes(routes),
})
return { pushed: routes.length, deleted: deleteIds.length }
})
}
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 {
getServer: (id: number) => Promise<ServerRow | undefined>
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" })
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
try {
const routes = mergeCachedCountry(parseRecursiveCatalog(await deps.readRaw(server)), await deps.getRoutes(serverId))
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" })
const serverId = parsed.data.serverId, server = await deps.getServer(serverId)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
try {
return reply.send({ serverId, gateways: parseRecursiveGatewayCatalog(await deps.readRaw(server)) })
} catch (error) {
@@ -227,6 +229,7 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
if (serverId === null) return reply.status(400).send({ error: "serverId is required" })
const server = await deps.getServer(serverId)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (!server.enabled) return reply.status(409).send({ error: "Устройство отключено: опрос и применение маршрутов недоступны" })
try {
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" })
const server = await deps.getServer(serverId)
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)
try {
@@ -290,14 +294,17 @@ export function createRecursiveRoutes(overrides: Partial<RecursiveRouteDependenc
const body = parsed.data, serverId = body.serverId
const server = await deps.getServer(serverId)
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 source: ConfigRevisionSource = body.source === "copy" ? "copy" : "apply"
let result: { pushed: number; deleted: number } | undefined
try {
const result = await deps.apply(server, routes, source)
result = await deps.apply(server, routes, source)
const stored = await deps.getRoutes(serverId)
return reply.send({ ok: true, serverId, routes: stored, pushedRoutes: result.pushed })
} 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: "Версия содержит некорректные маршруты; восстановление не выполнено" })
const server = await deps.getServer(rev.serverId)
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) => ({
...row, id: `rev-${index}`, comment: userRecursiveComment(row.comment),
}))
let result: { pushed: number; deleted: number } | undefined
try {
const result = await deps.apply(server, routes, "rollback")
result = await deps.apply(server, routes, "rollback")
const stored = await deps.getRoutes(server.id)
return reply.send({ ok: true, serverId: server.id, routes: stored, pushedRoutes: result.pushed })
} 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")
+46
View File
@@ -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 { 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+$/)])
.transform(Number).refine(value => Number.isSafeInteger(value) && value > 0)
export const recursiveTargetSchema = z.object({ serverId })
export const recursiveInputRowSchema = z.object({
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),
scope: 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(),
})
export const recursiveInputRowsSchema = z.array(recursiveInputRowSchema)
+32
View File
@@ -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.
Продолжение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
View File
@@ -1,6 +1,6 @@
# План реализации 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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+34 -2
View File
@@ -1,6 +1,6 @@
# 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).
@@ -36,7 +36,39 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–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)
+32
View File
@@ -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.
Продолжение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 открыт.
+3 -1
View File
@@ -1,6 +1,6 @@
# Тестовый сервер 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.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