From 338335ddc0de61d7f1fd5cd59e2ff96cd0dc641c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 00:53:40 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=8F=D1=82=D1=8C=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20Router=20ID=20?= =?UTF-8?q?=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/routes/ospf.ts | 26 +++------------- .../services/ospf-optimizer-identity.test.ts | 14 +++++++++ .../src/services/ospf-optimizer-identity.ts | 31 +++++++++++++++++++ docs/ux-ui-progress.md | 9 +++++- docs/ux-ui-stage-10.md | 4 +++ 6 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 backend/src/services/ospf-optimizer-identity.test.ts create mode 100644 backend/src/services/ospf-optimizer-identity.ts diff --git a/backend/package.json b/backend/package.json index b65ccfc9..a6ea168c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,7 @@ "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts", "test": "npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file", "test:geoip": "tsx src/services/traffic-flow-geoip.test.ts", - "test:ospf-optimizer": "tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts" + "test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", diff --git a/backend/src/routes/ospf.ts b/backend/src/routes/ospf.ts index 8405e523..98111e38 100644 --- a/backend/src/routes/ospf.ts +++ b/backend/src/routes/ospf.ts @@ -11,6 +11,7 @@ import type { } from "../types/server.js" import { parseOspfGateway, parseOspfRouteType, parseOspfCatalogRouteType } from "../services/ospf-route-parse.js" import { z } from "zod" +import {resolveOptimizerRouterOwners} from "../services/ospf-optimizer-identity.js" import {readOspfOptimizerObserved} from "../services/ospf-optimizer-metrics.js" import type {OspfOptimizerObserved} from "@mmapp/contracts" import {readEditableOspfTemplates,validateOspfOptimizationPlan} from "../services/ospf-optimizer-plan.js" @@ -362,27 +363,10 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number): async function buildRouterIdToServerMap(needed: Set): Promise> { const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true)) - const out = new Map() - const neededLeft = new Set(needed) - - for (const s of enabledServers) { - if (neededLeft.size === 0) break - try { - const c = MikrotikClient.fromServer(s) - const inst = await c.getOspfInstances() - for (const i of inst) { - const rid = String(i["router-id"] ?? "").trim() - if (!rid) continue - if (neededLeft.has(rid)) { - out.set(rid, s.id) - neededLeft.delete(rid) - } - } - } catch { - // ignore per-server errors, preview should still work - } - } - return out + return resolveOptimizerRouterOwners(enabledServers,needed,async id=>{ + const target=enabledServers.find(server=>server.id===id)! + return MikrotikClient.fromServer(target).getOspfInstances() + }) } const routerIdToServerId = await buildRouterIdToServerMap(neededRouterIds) diff --git a/backend/src/services/ospf-optimizer-identity.test.ts b/backend/src/services/ospf-optimizer-identity.test.ts new file mode 100644 index 00000000..b993cfc8 --- /dev/null +++ b/backend/src/services/ospf-optimizer-identity.test.ts @@ -0,0 +1,14 @@ +import assert from "node:assert/strict" +import {resolveOptimizerRouterOwners} from "./ospf-optimizer-identity.js" +const devices=[{id:1},{id:2}],needed=new Set(["192.0.2.1"]) +const instance={"router-id":"192.0.2.1",disabled:"false"} +let calls:number[]=[] +const resolved=await resolveOptimizerRouterOwners(devices,needed,async id=>{calls.push(id);return id===1?[instance,instance]:[{"router-id":"192.0.2.2",disabled:"false"}]}) +assert.equal(resolved.get("192.0.2.1"),1);assert.deepEqual(calls,[1,2]) +await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async()=>[instance]),/нескольким/) +await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async id=>{if(id===2)throw new Error("offline");return [instance]}),/проверить/) +for(const raw of [null,{},[{"router-id":"main",disabled:"false"}],[{"router-id":"0.0.0.0",disabled:"false"}],[{"router-id":"192.0.2.1"}],[{"router-id":"192.00.2.1",disabled:"false"}]])await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async()=>raw)) +assert.equal((await resolveOptimizerRouterOwners(devices,needed,async()=>[{disabled:"true"}])).size,0) +assert.equal((await resolveOptimizerRouterOwners(devices,needed,async()=>[])).size,0) +calls=[];await resolveOptimizerRouterOwners(devices,new Set(),async id=>{calls.push(id);return []});assert.equal(calls.length,0) +console.log("OSPF optimizer identity: complete inventory, literal IDs, duplicate ownership/unknown/failed refusal; no early first-match") diff --git a/backend/src/services/ospf-optimizer-identity.ts b/backend/src/services/ospf-optimizer-identity.ts new file mode 100644 index 00000000..8c8557b9 --- /dev/null +++ b/backend/src/services/ospf-optimizer-identity.ts @@ -0,0 +1,31 @@ +import {z} from "zod" +import {observedBoolean} from "./ospf-observed-values.js" +const schema=z.array(z.object({"router-id":z.string().optional(),disabled:z.string().optional()})) +function literal(value:string|undefined){ + const id=value?.trim(),parts=id?.split(".") + return id && id!=="0.0.0.0" && parts?.length===4 && parts.every(part=>/^(0|[1-9]\d{0,2})$/.test(part)&&Number(part)<=255)?id:null +} +export async function resolveOptimizerRouterOwners(devices:readonly {id:number}[],needed:Set,read:(id:number)=>Promise):Promise>{ + if(needed.size===0)return new Map() + const owners=new Map>() + for(const device of devices){ + let raw:unknown + try {raw=await read(device.id)} catch {throw new Error("Не удалось проверить Router ID устройства "+device.id)} + const parsed=schema.safeParse(raw) + if(!parsed.success)throw new Error("Недостоверный каталог OSPF устройства "+device.id) + for(const instance of parsed.data){ + const disabled=observedBoolean(instance.disabled) + if(disabled===true)continue + const id=literal(instance["router-id"]) + if(disabled===null||id===null)throw new Error("Неизвестен Router ID или disabled устройства "+device.id) + const set=owners.get(id)??new Set();set.add(device.id);owners.set(id,set) + } + } + const result=new Map() + for(const id of needed){ + const set=owners.get(id) + if(set && set.size>1)throw new Error("Router ID "+id+" принадлежит нескольким устройствам") + if(set?.size===1)result.set(id,[...set][0]) + } + return result +} diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index fa4f6b47..70831208 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -26,11 +26,18 @@ | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | -| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.40; OSPF provenance добавлен, далее идентичность Router ID и визуальная приёмка | +| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.41; Router ID проверяется без first-match, далее визуальная приёмка | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 10.41 — однозначное устройство OSPF (2026-10-01) + +- Сопоставление Router ID больше не останавливается на первом совпадении. Проверяются все enabled устройства: нужен literal ID активного экземпляра, повтор на другом устройстве отклоняется; неизвестный disabled/ID либо отказ опроса сообщает ошибку до write. Повторы одного ID внутри одного устройства допустимы; known disabled исключены. Без соседских Router ID дополнительный сетевой опрос не нужен. +- Изменена достоверность контекста, формулы score/cost и порядок apply прежние. Для RouterOS с динамическим/symbolic Router ID или отсутствующими flags потребуется отдельная проверка фактического каталога; их не угадываем по host. +- Backend optimizer identity/metrics/parser/HTTP tests и production backend прошли. Lint без ошибок, два прежних unused warning в ospf.ts. Реальных RouterOS запросов не выполняли. +- Далее обновить 1440/390/320 screenshots и выполнить локальную приёмку оптимизатора; затем карта сети. + ## Блок 10.40 — происхождение метрик OSPF API (2026-10-01) - Общий контракт @mmapp/contracts: value/source/sourceId/measuredAt для Ping/DL/UL. Источники speed-probe, interface-traffic, unknown; неизвестность не становится нулём. Задержка трафиком не измеряется. Backend добавляет observed данные до валидации ranked plan; некорректные данные отклоняются до write. Ranking/model defaults прежние. diff --git a/docs/ux-ui-stage-10.md b/docs/ux-ui-stage-10.md index e3ee152f..cbe0ed28 100644 --- a/docs/ux-ui-stage-10.md +++ b/docs/ux-ui-stage-10.md @@ -212,3 +212,7 @@ server сохраняет выбор/reload/history без потери друг ## 10.40. OSPF provenance API Shared контракт и backend observed исходные значения/даты, UI модель отдельно от снимка. Legacy без observed даёт неизвестность; rank/write прежние. Backend optimizer/live-maps, 30 E2E, TS/lint и обе production сборки прошли. Далее Router ID ambiguity, затем visual acceptance. + +## 10.41. Однозначность Router ID + +Полный опрос enabled inventory без first-match; duplicate ownership/unknown/failed каталог отклоняет план до write. Known disabled/empty и одинаковый ID одного устройства допустимы. Optimizer suite/backend build прошли, lint два прежних warning. Реальный RouterOS каталог ещё внешняя приёмка; далее screenshots.