fix(routing): исключить опрос выключенных устройств BGP и OSPF

This commit is contained in:
Denozordec committed 2026-10-09 17:22:41 +07:00
1 parent 89de7e134b
commit 8795969871
10 files changed
+299 -13

No files matched your search

+12
View File
@@ -50,3 +50,15 @@ assert.equal((await app.inject({ method: "GET", url: "/api/servers/0/bgp/session
assert.equal(reads, before)
await app.close()
console.log("BGP catalog: partial/empty/failed/context/duplicates and HTTP compatibility/503 passed")
for (const type of ["home-router", "exit-node"]) {
let enabled = false, polled = 0
const testApp = Fastify(); testApp.setValidatorCompiler(validatorCompiler); testApp.setSerializerCompiler(serializerCompiler)
await testApp.register(createBgpRoutes({listEnabledServers:async()=>[],findServer:async id=>id===1?({id:1,name:"test",host:"192.0.2.1",enabled,type} as typeof servers.$inferSelect):undefined,readSessions:async()=>{polled++;return []},readRawSessions:async()=>{polled++;return []}}),{prefix:"/api"})
for (const url of ["/api/bgp/sessions/raw/1","/api/servers/1/bgp/sessions"]) {const r=await testApp.inject({method:"GET",url});assert.equal(r.statusCode,409);assert.match(r.json().error,/выключен/)}
assert.equal(polled,0)
for(const url of ["/api/bgp/sessions/raw/99","/api/servers/99/bgp/sessions"])assert.equal((await testApp.inject({method:"GET",url})).statusCode,404)
assert.equal((await testApp.inject({method:"GET",url:"/api/bgp/sessions/raw/1oops"})).statusCode,400);assert.equal(polled,0)
enabled=true;assert.equal((await testApp.inject({method:"GET",url:"/api/bgp/sessions/raw/1"})).statusCode,200);assert.equal(polled,1)
await testApp.close()
}
console.log("BGP selected/raw: disabled home-router/exit-node do not poll; unknown/invalid IDs and enabled read preserved")
+7 -4
View File
@@ -15,6 +15,7 @@ interface BgpDependencies {
findServer: (id: number) => Promise<ServerRow | undefined>
readCatalogSessions?: (server: ServerRow) => Promise<BgpCatalogSessionRead[]>
readSessions: (server: ServerRow) => Promise<BgpSessionRead[]>
readRawSessions?: (server: ServerRow) => Promise<unknown[]>
}
const defaults: BgpDependencies = {
listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)),
@@ -33,12 +34,13 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
app.get("/bgp/sessions/raw/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0]
const server = await dependencies.findServer(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос BGP не выполнялся" })
try {
const client = MikrotikClient.fromServer(server)
const raw = await client.getBgpSessionsRaw()
app.log.info({ serverId: server.id, host: server.host, raw }, "BGP raw response")
const raw = await (dependencies.readRawSessions ?? (s => MikrotikClient.fromServer(s).getBgpSessionsRaw()))(server)
if (!Array.isArray(raw)) throw new Error("Некорректный список BGP sessions")
app.log.info({ serverId: server.id, sessionCount: raw.length }, "BGP raw response")
return reply.send({ server: { id: server.id, host: server.host, name: server.name }, raw })
} catch (err) {
return reply.status(500).send({ error: String(err) })
@@ -58,6 +60,7 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti
const server = await dependencies.findServer(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос BGP не выполнялся" })
try {
const catalog = await pollBgpCatalog([server], dependencies.readSessions)
+14
View File
@@ -71,3 +71,17 @@ assert.equal(parseBfdCatalogSessions(bfdServer, rawBfd("up"))[0].multihop, null)
assert.equal(parseBfdCatalogSessions(bfdServer, [{ ...rawBfd("up")[0], multihop: "false" }])[0].multihop, false)
assert.equal(parseCatalogRoutes(bfdServer, [rawRoute])[0].type, "Unknown")
assert.equal(parseCatalogRoutes(bfdServer, [{ ...rawRoute, "ospf-type": "intra-area" }])[0].type, "O")
for (const type of ["home-router", "exit-node"]) {
const {MikrotikClient}=await import("../services/mikrotik.js");const original=MikrotikClient.fromServer;let reads=0
MikrotikClient.fromServer=()=>{reads++;throw Error("must not poll")}
const testApp=Fastify();testApp.setValidatorCompiler(validatorCompiler);testApp.setSerializerCompiler(serializerCompiler)
try {await testApp.register(createOspfRoutes({listEnabledServers:async()=>[],readCatalog:async()=>{throw Error("must not poll")},findServer:async id=>id===1?({id:1,name:"test",enabled:false,type} as typeof servers.$inferSelect):undefined}),{prefix:"/api"});const r=await testApp.inject({method:"GET",url:"/api/servers/1/ospf"});assert.equal(r.statusCode,409);assert.match(r.json().error,/выключен/);assert.equal((await testApp.inject({method:"GET",url:"/api/servers/99/ospf"})).statusCode,404);assert.equal((await testApp.inject({method:"GET",url:"/api/servers/1oops/ospf"})).statusCode,400);assert.equal(reads,0)}finally{await testApp.close();MikrotikClient.fromServer=original}
}
console.log("OSPF selected: disabled home-router/exit-node do not poll; unknown/invalid IDs fail before RouterOS")
{
const {MikrotikClient}=await import("../services/mikrotik.js"),original=MikrotikClient.fromServer
MikrotikClient.fromServer=()=>({getOspfNeighbors:async()=>[],getOspfAreas:async()=>[],getOspfInterfaceTemplates:async()=>[],getOspfInstances:async()=>[],getBfdSessions:async()=>{throw Error("BFD unavailable")},getIpRoutes:async()=>[]} as unknown as MikrotikClient)
const testApp=Fastify();testApp.setValidatorCompiler(validatorCompiler);testApp.setSerializerCompiler(serializerCompiler)
try{await testApp.register(createOspfRoutes({listEnabledServers:async()=>[],readCatalog:async()=>emptyOspfCatalog(),findServer:async()=>({id:1,name:"test",enabled:true} as typeof servers.$inferSelect)}),{prefix:"/api"});const r=await testApp.inject({method:"GET",url:"/api/servers/1/ospf"});assert.equal(r.statusCode,503);assert.match(r.json().error,/BFD unavailable/)}finally{await testApp.close();MikrotikClient.fromServer=original}
}
console.log("OSPF selected: failed BFD does not become successful empty data")
+5 -3
View File
@@ -553,6 +553,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
interface OspfCatalogDependencies {
listEnabledServers: () => Promise<ServerRow[]>
readCatalog: (server: ServerRow) => Promise<OspfCatalogCollections>
findServer?: (id: number) => Promise<ServerRow | undefined>
getOptimizationServer?: (id:number)=>Promise<ServerRow | undefined>
buildOptimizationPlan?: (server:ServerRow,weight:number)=>Promise<OspfRankedInterface[]>
setTemplateCost?: (server:ServerRow,id:string,cost:number)=>Promise<void>
@@ -676,15 +677,16 @@ export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalog
// GET /api/servers/:id/ospf — single server OSPF + BFD data
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = (await db
const server = dependencies.findServer ? await dependencies.findServer(params.id) : (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос OSPF не выполнялся" })
try {
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(server)
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await readOspfCatalogRaw(MikrotikClient.fromServer(server))
const areaMap = buildAreaMap(areas)
return reply.send({
neighbors: parseNeighbors(server, neighbors, areaMap),
@@ -695,7 +697,7 @@ export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalog
areas: areas.map(a => ({ name: a.name, areaId: a["area-id"] ?? "0.0.0.0", type: a.type, disabled: a.disabled === "true", inactive: a.inactive === "true", instance: a.instance })),
})
} catch (err) {
return reply.status(500).send({ error: String(err) })
return reply.status(503).send({ error: String(err) })
}
})
+3 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Статус на2026-10-09: backend ux-ipsec82c/frontend ux-ipsec77, backend healthy; основной блок IPsec77 и VXLAN76 приняты в описанных границах. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен.
Статус на2026-10-09: backend ux-routing83/frontend ux-ipsec77, backend healthy; основной блок IPsec77 и VXLAN76 приняты в описанных границах. В12.5.67 исправленный Peer .conf принят как клиентский шаблон: API6/6, browser4/4, server endpoint и placeholders; настоящий RSC roundtrip через UI/native/handshake/Ping принят в12.5.68, .conf с явнымName/отказы400/409/сохранение формы приняты в70; поздний native rejection/подтверждённая компенсация/retry приняты в72; изолированный audit INSERT permission denial принят в74; остальные неизвестные исходы/DB faults открыты. Мобильная ширина WireGuard Sheets принята в12.5.64. Мобильная GRE форма и native сохранение на обоих CHR приняты в12.5.57. WireGuard handshake/короткий Ping/native API/read-only UI приняты в границах12.5.58; 12.5.59 воспроизвёл оставшиеся peers/IP; 12.5.60 развернул исправление и принял обычное удаление own WG интерфейса с peer/IPv4 адресом на обоих CHR. 12.5.62 принял полный WireGuard UI create/peer/disable/enable/Ping/delete на обоих CHR; устойчивое меню развёрнуто. Устойчивость меню GRE при live refresh на обоих CHR принята в12.5.55. GRE peer metadata и реальный lifecycle/API/UI/двусторонний Ping подтверждены в12.5.53. Browser retry/checkpoint на реальном HTTP/PG принят в границах12.5.43–45; Telegram sender responses подменены, реальная доставка не проверена. PG-1/2/3/4/5 приняты в описанных границах, PG-6 integrated для одного isolated instance, PG-7 real JWT/processcounts и browser restore passed. PG-8 real PG/native и browser unknown→history done приняты с fault injection после настоящего COMMIT. ROS-8/9 real API/SQL/native приняты в границах12.5.23; browser pair/duration change и delete приняты в12.5.24. ROS-2 authentication failure принят в12.5.25. PortalSSO, физический TCP disconnect, мультиэкземпляры и остальные ROS/Q/интеграции остаются открытыми. [Паспорт](ux-ui-test-server.md), [журнал](ux-ui-progress.md). VXLAN75: собственная native пара/Ping/catalog/URL/export приняты, create/delete API отсутствует; selected baseline восстановлен.
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
@@ -12,6 +12,8 @@ IPsec81: partial/unknown native fault injection, preflight конфликтов,
IPsec82: production certificate data plane/NAT Ping3/3, capped UDP3s, history group/egress и API server delete приняты. Baseline14 коллекций×2 восстановлен после route convergence; own/private fixtures удалены. [Отчёт](ux-ui/acceptance/ipsec-network-2026-10-09.json).
Routing83: disabled BGP/OSPF selected/raw409/probe0 и собственная eBGP Established пара с reject-all filters приняты; original routes/config preserved, own DB/native fixtures удалены. [Отчёт](ux-ui/acceptance/routing-bgp-disabled-2026-10-09.json).
## Паспорт стенда
Перед внешним запуском зафиксировать отдельные PostgreSQL и RouterOS тестовые устройства, версии server/client PostgreSQL и RouterOS, HEAD backend/frontend, роли пользователей, число экземпляров backend/планировщика/отправителей и способ их остановки. Для Telegram использовать тестовый чат, для остальных интеграций — тестовые endpoints и учётные записи. Зафиксировать ожидаемые изменения и способ восстановления стенда.
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-09: 12.5.82 исправил NAT без обязательного WAN, изолировал IPsec policy group и сохранил ограничения NAT/группы в истории. Production API init/client/restore/delete, сертификатный data plane, NAT Ping3/3 и capped bandwidth3s приняты; baseline восстановлен после ожидания динамических маршрутов. Backend ux-ipsec82c healthy, frontend ux-ipsec77. Следующий12.5.83 — BGP/OSPF selected/diagnostic paths: запрет опроса выключенных устройств и достоверность ошибок; затем реальные профильные сценарии маршрутизации. Этап12.5 открыт.
Актуальное продолжение 2026-10-09: 12.5.83 закрыл опрос выключенных устройств в BGP raw/selected и OSPF selected; выбранный OSPF сообщает отказ BFD/routes. Real disabled probe0, eBGP Established на двух CHR с reject-all input/output и baseline cleanup приняты. Backend ux-routing83 healthy, frontend ux-ipsec77. Следующий12.5.84 — реальная OSPF adjacency/маршрут через собственный GRE; этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-09. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+16 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-09: 12.5.82 исправил NAT без обязательного WAN, изолировал IPsec policy group и сохранил ограничения NAT/группы в истории. Production API init/client/restore/delete, сертификатный data plane, NAT Ping3/3 и capped bandwidth3s приняты; baseline восстановлен после ожидания динамических маршрутов. Backend ux-ipsec82c healthy, frontend ux-ipsec77. Следующий12.5.83 — BGP/OSPF selected/diagnostic paths: запрет опроса выключенных устройств и достоверность ошибок; затем реальные профильные сценарии маршрутизации. Этап12.5 открыт.
Актуальное продолжение 2026-10-09: 12.5.83 закрыл опрос выключенных устройств в BGP raw/selected и OSPF selected; выбранный OSPF сообщает отказ BFD/routes. Real disabled probe0, eBGP Established на двух CHR с reject-all input/output и baseline cleanup приняты. Backend ux-routing83 healthy, frontend ux-ipsec77. Следующий12.5.84 — реальная OSPF adjacency/маршрут через собственный GRE; этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -36,7 +36,21 @@
| 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–82 и ограничения ниже. Runtime backend ux-ipsec82c/frontend ux-ipsec77. |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–83 и ограничения ниже. Runtime backend ux-routing83/frontend ux-ipsec77. |
## 12.5.83 — маршрутизация: выключенные устройства и реальная eBGP-пара (2026-10-09)
**Изменения.** BGP raw debug использует общую lookup зависимость и отклоняет enabled=false до создания клиента. Selected BGP sessions и selected OSPF также возвращают409 без опроса, независимо от типа Home Router/Exit Node. BGP debug журнал содержит число сессий вместо дублирования полного raw payload. Selected OSPF использует strict readOspfCatalogRaw: отказ BFD/routes или неправильный контейнер больше не подменяется успешными пустыми массивами; dependency failure503. Legacy aggregate paths не менялись этим блоком.
**Проверки.** BGP/OSPF catalog tests с новыми selected/raw disabled/unknown/invalid/enabled сценариями, OSPF BFD failure503; backend TypeScript, scoped ESLint/diff check passed. Сохраняются исходные OSPF unused and/measurePingByInterface warnings, новых ошибок нет. UI/контракты не менялись, frontend build и полная регрессия не повторялись.
**Real API.** Две собственные disabled DB записи с HTTP probe на приватном Docker DNS: Home Router/Exit Node × BGP raw/selected/OSPF selected409, probeRequests0. Записи удалены по id/name/host. Selected OSPF двух настоящих enabled CHR возвращает корректные массивы; это чтение исходно пустой OSPF конфигурации, не приёмка adjacency.
**eBGP.** Собственные RouterOS7.24 instances с private ASN65083/65084 и конкретными remote/local public IP, multihop eBGP connections и input/output reject-all chains. Native established=true на обоих CHR, production selected и catalog State Established/type eBGP/ASNs корректны; received prefixes0, catalog transmitted prefixes null как ненаблюдаемое значение. Инстансы/соединения/filter rules удалены по точным ID/name/chain/comment после полного preflight. Исходные ID connection/instance/filter/IPv4 route коллекций совпали на обоих CHR, default/management routes не участвовали в обмене. Это Establishment/read-only UI data contract, не route policy propagation, failover или maximum scale. [Отчёт](ux-ui/acceptance/routing-bgp-disabled-2026-10-09.json).
**Runtime.** Одна backend Docker сборка, entry/node --check/pg_restore18.6 и compose --wait passed. mmapp-test/backend:ux-routing83/version1.0.0-test.routing83, image sha256:b68c326b8ee601eb2c490e7517c49614abd8cdfccc9d2af2ec44fb5d3dc5aded7, healthy. Frontend ux-ipsec77 сохранён. Context ipsec82c +четыре routing backend файла рабочего состояния до коммита, archive eaaca441a3eae1c95396abaca4a09fa4a38e4a769adaff276fc4bbcadac18244; label routing83, не итоговый Git SHA. .env backup приватный; миграций нет. Один локальный коммит без push.
Следующий84 — OSPF Full adjacency и обучение только собственного частного prefix через собственный GRE; исходные public interfaces/default routes сохранить. BGP failover/route propagation/BFD optimizer/recursive diagnostics и внешние границы IPsec82 остаются открытыми. Версия RouterOS7.20+ с явными instance подтверждена [MikroTik BGP](https://help.mikrotik.com/docs/spaces/ROS/pages/328220/BGP) и настоящим API7.24.5; схема таблиц/деталей сохраняет референсы Data Grid Filtering2/List9 общего плана.
## 12.5.82 — IPsec: NAT, policy group, реальная передача и полный цикл (2026-10-09)
+14
View File
@@ -1968,3 +1968,17 @@ flowchart LR
**Runtime/происхождение.** Backend mmapp-test/backend:ux-ipsec82c/version1.0.0-test.ipsec82c, image sha256:57fdd7dbbf66091fd0710154ed7c47f284047505977e7b06d0b15bfbfd5fa57d3, healthy; frontend ux-ipsec77. Три builds82/82b/82c по новым подтверждённым отказам: NAT/WAN, proposal group, empty list restore. Финальный context82b +11 backend файлов рабочего состояния, archive d7b0ad10c248ca6c1525266d67b4953f2752f5d046d46b63262e3dc93421e280, label ipsec82c не итоговый Git SHA. Entry/node --check/pg_restore18.6 и compose --wait passed; apt cached. Приватные .env backups, миграций нет. Один локальный коммит без push.
Основной реальный цикл IPsec принят в перечисленных границах. Открыты сложные/экранированные DN, реальные клиентские ОС/IPv6, Portal SSO/browser auth, физические disconnect/durable retry/multi-instance, maximum throughput и полный PKI/policy/profile/proposal restore. Следующий83 — маршрутизация. Референсы интерфейса сохраняются: List9/Sheet8 из общего плана; настройки полей подтверждены [RouterOS IPsec](https://help.mikrotik.com/docs/spaces/ROS/pages/11993097/IPsec) и [Bandwidth Test](https://help.mikrotik.com/docs/spaces/ROS/pages/7962644/Bandwidth%2BTest).
## 12.5.83 — маршрутизация: выключенные устройства и реальная eBGP-пара (2026-10-09)
**Изменения.** BGP raw debug использует общую lookup зависимость и отклоняет enabled=false до создания клиента. Selected BGP sessions и selected OSPF также возвращают409 без опроса, независимо от типа Home Router/Exit Node. BGP debug журнал содержит число сессий вместо дублирования полного raw payload. Selected OSPF использует strict readOspfCatalogRaw: отказ BFD/routes или неправильный контейнер больше не подменяется успешными пустыми массивами; dependency failure503. Legacy aggregate paths не менялись этим блоком.
**Проверки.** BGP/OSPF catalog tests с новыми selected/raw disabled/unknown/invalid/enabled сценариями, OSPF BFD failure503; backend TypeScript, scoped ESLint/diff check passed. Сохраняются исходные OSPF unused and/measurePingByInterface warnings, новых ошибок нет. UI/контракты не менялись, frontend build и полная регрессия не повторялись.
**Real API.** Две собственные disabled DB записи с HTTP probe на приватном Docker DNS: Home Router/Exit Node × BGP raw/selected/OSPF selected409, probeRequests0. Записи удалены по id/name/host. Selected OSPF двух настоящих enabled CHR возвращает корректные массивы; это чтение исходно пустой OSPF конфигурации, не приёмка adjacency.
**eBGP.** Собственные RouterOS7.24 instances с private ASN65083/65084 и конкретными remote/local public IP, multihop eBGP connections и input/output reject-all chains. Native established=true на обоих CHR, production selected и catalog State Established/type eBGP/ASNs корректны; received prefixes0, catalog transmitted prefixes null как ненаблюдаемое значение. Инстансы/соединения/filter rules удалены по точным ID/name/chain/comment после полного preflight. Исходные ID connection/instance/filter/IPv4 route коллекций совпали на обоих CHR, default/management routes не участвовали в обмене. Это Establishment/read-only UI data contract, не route policy propagation, failover или maximum scale. [Отчёт](ux-ui/acceptance/routing-bgp-disabled-2026-10-09.json).
**Runtime.** Одна backend Docker сборка, entry/node --check/pg_restore18.6 и compose --wait passed. mmapp-test/backend:ux-routing83/version1.0.0-test.routing83, image sha256:b68c326b8ee601eb2c490e7517c49614abd8cdfccc9d2af2ec44fb5d3dc5aded7, healthy. Frontend ux-ipsec77 сохранён. Context ipsec82c +четыре routing backend файла рабочего состояния до коммита, archive eaaca441a3eae1c95396abaca4a09fa4a38e4a769adaff276fc4bbcadac18244; label routing83, не итоговый Git SHA. .env backup приватный; миграций нет. Один локальный коммит без push.
Следующий84 — OSPF Full adjacency и обучение только собственного частного prefix через собственный GRE; исходные public interfaces/default routes сохранить. BGP failover/route propagation/BFD optimizer/recursive diagnostics и внешние границы IPsec82 остаются открытыми. Версия RouterOS7.20+ с явными instance подтверждена [MikroTik BGP](https://help.mikrotik.com/docs/spaces/ROS/pages/328220/BGP) и настоящим API7.24.5; схема таблиц/деталей сохраняет референсы Data Grid Filtering2/List9 общего плана.
+4 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-09
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-ipsec82c/version1.0.0-test.ipsec82c, frontend ux-ipsec77/version1.0.0-test.ipsec77; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
Сервер161.104.46.222 предоставлен для временного тестирования. Backend ux-routing83/version1.0.0-test.routing83, frontend ux-ipsec77/version1.0.0-test.ipsec77; backend/PG healthy, frontend HTTP/browser passed. Основной блок IPsec77 принят в границах журнала, own fixtures/fake DB rows удалены; каталог flash CHR2 сохранён из-за неподтверждённого ownership. SSH/оба CHR доступны, пароли прежние.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -14,7 +14,7 @@
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
| Образы приложения | mmapp-test/backend:ux-ipsec82c и mmapp-test/frontend:ux-ipsec77; запущены |
| Образы приложения | mmapp-test/backend:ux-routing83 и mmapp-test/frontend:ux-ipsec77; запущены |
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
@@ -28,6 +28,8 @@ IPsec81: partial/unknown native fault injection, preflight конфликтов,
IPsec82: production certificate data plane/NAT Ping3/3, capped UDP3s, history group/egress и API server delete приняты. Baseline14 коллекций×2 восстановлен после route convergence; own/private fixtures удалены. [Отчёт](ux-ui/acceptance/ipsec-network-2026-10-09.json).
Routing83: disabled BGP/OSPF selected/raw409/probe0 и собственная eBGP Established пара с reject-all filters приняты; original routes/config preserved, own DB/native fixtures удалены. [Отчёт](ux-ui/acceptance/routing-bgp-disabled-2026-10-09.json).
## Развёртывание приложения
Чистые frontend/backend build contexts подготовлены по .gitea/workflows/docker.yml из закреплённого Git HEAD;образы собраны на сервере без push. Локально docker CLI сейчас отсутствует, на сервере доступен Docker. В корневом .dockerignore исключён backend, поэтому обычный backend build из корня не повторяет CI: использовать отдельный backend context, как main workflow. Не передавать .env.local,backend/.env,ключи и локальные node_modules/.next.
@@ -0,0 +1,223 @@
{
"date": "2026-10-09",
"name": "mm-routing83-1791540955934",
"disabled": [
{
"type": "home-router",
"path": "/bgp/sessions/raw/:id",
"status": 409
},
{
"type": "home-router",
"path": "/servers/:id/bgp/sessions",
"status": 409
},
{
"type": "home-router",
"path": "/servers/:id/ospf",
"status": 409
},
{
"type": "exit-node",
"path": "/bgp/sessions/raw/:id",
"status": 409
},
{
"type": "exit-node",
"path": "/servers/:id/bgp/sessions",
"status": 409
},
{
"type": "exit-node",
"path": "/servers/:id/ospf",
"status": 409
}
],
"bgp": [
{
"serverId": 1,
"native": [
{
".id": "*2800001",
"name": "mm-routing83-1791540955934-1",
"established": "true",
"remote.address": "104.165.244.7",
"remote.as": "65084",
"local.address": "194.226.121.150",
"local.as": "65083",
"uptime": "20ms",
"prefix-count": "0",
"input.filter": "mm-routing83-1791540955934",
"output.filter-chain": "mm-routing83-1791540955934"
}
],
"selected": [
{
"id": "*2800001",
"serverId": 1,
"serverName": "CHR",
"serverSite": "",
"serverCountry": "",
"name": "mm-routing83-1791540955934-1",
"peerIp": "104.165.244.7",
"remoteAs": 65084,
"localAs": 65083,
"localId": "194.226.121.150",
"remoteId": "104.165.244.7",
"state": "Established",
"type": "eBGP",
"uptime": "20ms",
"holdTime": 180,
"keepalive": 60,
"prefixesRx": 0,
"prefixesTx": 0,
"inputMessages": 1,
"outputMessages": 1,
"capabilities": [
"MP-BGP",
"Route Refresh",
"Extended Next-Hop",
"Graceful Restart",
"4-byte-AS"
],
"lastError": null
}
],
"catalog": [
{
"id": "*2800001",
"serverId": 1,
"serverName": "CHR",
"serverSite": "",
"serverCountry": "",
"name": "mm-routing83-1791540955934-1",
"peerIp": "104.165.244.7",
"remoteAs": 65084,
"localAs": 65083,
"localId": "194.226.121.150",
"remoteId": "104.165.244.7",
"state": "Established",
"type": "eBGP",
"uptime": "20ms",
"holdTime": 180,
"keepalive": 60,
"prefixesRx": 0,
"prefixesTx": null,
"inputMessages": 1,
"outputMessages": 1,
"capabilities": [
"MP-BGP",
"Route Refresh",
"Extended Next-Hop",
"Graceful Restart",
"4-byte-AS"
],
"lastError": null,
"afi": "ip"
}
]
},
{
"serverId": 2,
"native": [
{
".id": "*2800001",
"name": "mm-routing83-1791540955934-1",
"established": "true",
"remote.address": "194.226.121.150",
"remote.as": "65083",
"local.address": "104.165.244.7",
"local.as": "65084",
"uptime": "250ms",
"prefix-count": "0",
"input.filter": "mm-routing83-1791540955934",
"output.filter-chain": "mm-routing83-1791540955934"
}
],
"selected": [
{
"id": "*2800001",
"serverId": 2,
"serverName": "CHR",
"serverSite": "",
"serverCountry": "",
"name": "mm-routing83-1791540955934-1",
"peerIp": "194.226.121.150",
"remoteAs": 65083,
"localAs": 65084,
"localId": "104.165.244.7",
"remoteId": "194.226.121.150",
"state": "Established",
"type": "eBGP",
"uptime": "250ms",
"holdTime": 180,
"keepalive": 60,
"prefixesRx": 0,
"prefixesTx": 0,
"inputMessages": 1,
"outputMessages": 1,
"capabilities": [
"MP-BGP",
"Route Refresh",
"Extended Next-Hop",
"Graceful Restart",
"4-byte-AS"
],
"lastError": null
}
],
"catalog": [
{
"id": "*2800001",
"serverId": 2,
"serverName": "CHR",
"serverSite": "",
"serverCountry": "",
"name": "mm-routing83-1791540955934-1",
"peerIp": "194.226.121.150",
"remoteAs": 65083,
"localAs": 65084,
"localId": "104.165.244.7",
"remoteId": "194.226.121.150",
"state": "Established",
"type": "eBGP",
"uptime": "250ms",
"holdTime": 180,
"keepalive": 60,
"prefixesRx": 0,
"prefixesTx": null,
"inputMessages": 1,
"outputMessages": 1,
"capabilities": [
"MP-BGP",
"Route Refresh",
"Extended Next-Hop",
"Graceful Restart",
"4-byte-AS"
],
"lastError": null,
"afi": "ip"
}
]
}
],
"cleanup": [
{
"serverId": 1,
"/routing/bgp/connection": true,
"/routing/bgp/instance": true,
"/routing/filter/rule": true,
"/ip/route": true
},
{
"serverId": 2,
"/routing/bgp/connection": true,
"/routing/bgp/instance": true,
"/routing/filter/rule": true,
"/ip/route": true
}
],
"disabledProbeRequests": 0,
"routingFilters": "input and output reject all; no routes advertised/accepted",
"dbFixtureRemoved": true
}