diff --git a/backend/package.json b/backend/package.json index 2cda905a..97b3d0a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/routes/recursive-routes.test.ts b/backend/src/routes/recursive-routes.test.ts index f074125d..16a3d24f 100644 --- a/backend/src/routes/recursive-routes.test.ts +++ b/backend/src/routes/recursive-routes.test.ts @@ -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>> +const server = { id: 1, name: "Тест", enabled: true, type: "home-router" } as NonNullable>> +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") diff --git a/backend/src/routes/recursive-routes.ts b/backend/src/routes/recursive-routes.ts index 474f1421..3447bdc1 100644 --- a/backend/src/routes/recursive-routes.ts +++ b/backend/src/routes/recursive-routes.ts @@ -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 { } 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("/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 readRaw: (server: ServerRow) => Promise @@ -201,6 +201,7 @@ export function createRecursiveRoutes(overrides: Partial ({ ...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 ({ ...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 }) } }) } diff --git a/backend/src/services/recursive-apply.test.ts b/backend/src/services/recursive-apply.test.ts new file mode 100644 index 00000000..fc2c8b28 --- /dev/null +++ b/backend/src/services/recursive-apply.test.ts @@ -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 = { + get: async () => rows as T, + put: async () => { 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") diff --git a/backend/src/services/recursive-apply.ts b/backend/src/services/recursive-apply.ts new file mode 100644 index 00000000..c16d5af0 --- /dev/null +++ b/backend/src/services/recursive-apply.ts @@ -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, + payloads: Record[], + persist: () => Promise, +) { + // The editor replaces every owned row: reject unsupported ownership before any write. + const existing = parseRecursiveCatalog(await client.get("/ip/route")) + const createdIds: string[] = [], deletedIds: string[] = [] + let nativeComplete = false + try { + for (const payload of payloads) { + const result = await client.put("/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, + }) + } +} diff --git a/backend/src/services/recursive-route-validation.ts b/backend/src/services/recursive-route-validation.ts index eaa47e4b..091c5983 100644 --- a/backend/src/services/recursive-route-validation.ts +++ b/backend/src/services/recursive-route-validation.ts @@ -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) diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index c61fd23c..64b1f5db 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -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 открыт. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index a30fd746..17d5de75 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -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). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index cf0d6cde..3df426e8 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -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) diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 4f470f65..255d4e17 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -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 открыт. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index dad6a285..23222847 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-09 -Сервер161.104.46.222 предоставлен для временного тестирования. Backend 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. diff --git a/docs/ux-ui/acceptance/browser-recursive-2026-10-09.json b/docs/ux-ui/acceptance/browser-recursive-2026-10-09.json new file mode 100644 index 00000000..bcd81c0c --- /dev/null +++ b/docs/ux-ui/acceptance/browser-recursive-2026-10-09.json @@ -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": [] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/recursive-routes-2026-10-09.json b/docs/ux-ui/acceptance/recursive-routes-2026-10-09.json new file mode 100644 index 00000000..4fac5798 --- /dev/null +++ b/docs/ux-ui/acceptance/recursive-routes-2026-10-09.json @@ -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 + } +} \ No newline at end of file diff --git a/docs/ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png b/docs/ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png new file mode 100644 index 00000000..3177b00f Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-form-server1-320-2026-10-09.png differ diff --git a/docs/ux-ui/screens/recursive-real-form-server2-320-2026-10-09.png b/docs/ux-ui/screens/recursive-real-form-server2-320-2026-10-09.png new file mode 100644 index 00000000..80f2cbfc Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-form-server2-320-2026-10-09.png differ diff --git a/docs/ux-ui/screens/recursive-real-server1-1280-2026-10-09.png b/docs/ux-ui/screens/recursive-real-server1-1280-2026-10-09.png new file mode 100644 index 00000000..fb0e5ff3 Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-server1-1280-2026-10-09.png differ diff --git a/docs/ux-ui/screens/recursive-real-server1-320-2026-10-09.png b/docs/ux-ui/screens/recursive-real-server1-320-2026-10-09.png new file mode 100644 index 00000000..203abf55 Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-server1-320-2026-10-09.png differ diff --git a/docs/ux-ui/screens/recursive-real-server2-1280-2026-10-09.png b/docs/ux-ui/screens/recursive-real-server2-1280-2026-10-09.png new file mode 100644 index 00000000..9ec5ff5b Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-server2-1280-2026-10-09.png differ diff --git a/docs/ux-ui/screens/recursive-real-server2-320-2026-10-09.png b/docs/ux-ui/screens/recursive-real-server2-320-2026-10-09.png new file mode 100644 index 00000000..2cf23f23 Binary files /dev/null and b/docs/ux-ui/screens/recursive-real-server2-320-2026-10-09.png differ