From 4f084222ad3c14df4a72128bb76e3c924de20ff3 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 14 Oct 2026 00:40:41 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=B0=D0=B1=D0=BB=D1=8E=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20BGP=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=BF=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- .../acceptance/bgp-complete-poll-sql.mjs | 34 +++++ backend/src/routes/bgp-catalog.test.ts | 3 +- backend/src/routes/bgp.ts | 3 + backend/src/services/bgp-peers-live.test.ts | 15 ++ backend/src/services/bgp-peers-live.ts | 30 ++-- docs/ux-ui-external-acceptance.md | 6 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 12 +- docs/ux-ui-remaining-acceptance.md | 10 +- docs/ux-ui-stage-12.md | 13 +- docs/ux-ui-test-server.md | 6 +- docs/ux-ui/acceptance/bgp216-baseline.json | 130 ++++++++++++++++++ .../bgp216-build-proof-2026-10-14.json | 24 ++++ .../bgp216-deployment-2026-10-14.json | 21 +++ docs/ux-ui/acceptance/bgp216-fixed.json | 130 ++++++++++++++++++ .../bgp216-initial-fixture-failure.json | 12 ++ .../acceptance/bgp216-live-2026-10-14.json | 50 +++++++ 18 files changed, 478 insertions(+), 25 deletions(-) create mode 100644 backend/scripts/acceptance/bgp-complete-poll-sql.mjs create mode 100644 backend/src/services/bgp-peers-live.test.ts create mode 100644 docs/ux-ui/acceptance/bgp216-baseline.json create mode 100644 docs/ux-ui/acceptance/bgp216-build-proof-2026-10-14.json create mode 100644 docs/ux-ui/acceptance/bgp216-deployment-2026-10-14.json create mode 100644 docs/ux-ui/acceptance/bgp216-fixed.json create mode 100644 docs/ux-ui/acceptance/bgp216-initial-fixture-failure.json create mode 100644 docs/ux-ui/acceptance/bgp216-live-2026-10-14.json diff --git a/backend/package.json b/backend/package.json index b1e44e1a..55e0b763 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "dev": "tsx watch src/index.ts", "build": "npm run build -w @mmapp/contracts && tsc", "test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts", - "test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts", + "test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/bgp-peers-live.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts", "start": "node dist/index.js", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", diff --git a/backend/scripts/acceptance/bgp-complete-poll-sql.mjs b/backend/scripts/acceptance/bgp-complete-poll-sql.mjs new file mode 100644 index 00000000..b717b176 --- /dev/null +++ b/backend/scripts/acceptance/bgp-complete-poll-sql.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import {createServer} from 'node:http' +import {randomBytes} from 'node:crypto' +const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL) +assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database) +process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own216.invalid';process.env.AUTH_PORTAL_URL='https://own216.invalid' +const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js'),{fetchBgpSessionsForAlerts}=await import('/app/backend/dist/services/bgp-peers-live.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js') +assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false)) +let phase='populated',app,nativeCalls=0,nativeWrites=0,disabledCalls=0;const cases=[] +const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');if(phase==='failed'||(phase==='partial'&&label==='second')){res.statusCode=401;res.end(JSON.stringify({error:401,message:'own216 fixture denied'}));return}const rows=phase==='populated'?[{'.id':'*1',name:'own216-peer',state:'established'}]:[];res.end(JSON.stringify(rows))}) +const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r)) +const baseline=process.env.MMAPP_BASELINE==='true';const report={date:'2026-10-14',block:'12.5.216',baseline,database,cases,scope:'Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged',passed:false} +try{ + await initDatabase();const ids=[];for(let i=0;if.serverId),expected);if(isCatalog)assert.equal(body.sessions.length,phase==='populated'?2:0)}else{assert(Array.isArray(body));assert.equal(body.length,phase==='populated'?2:0)} + if(phase==='disabled')assert.equal(nativeCalls,before) + cases.push({phase,endpoint,status:r.statusCode,failures:body.failures?.length??0,nativeCalls:nativeCalls-before}) + } + const snapshot=await collectGreBgpSnapshotOnce({fetchGre:async()=>[{targetLabel:'own216-GRE-control',status:'up'}],fetchBgp:fetchBgpSessionsForAlerts}) + assert.equal(snapshot.greWritten,1);assert.equal(snapshot.bgpWritten,phase==='populated'?2:0);assert.equal(snapshot.errors.length,!baseline&&hasFailure?1:0) + if(snapshot.errors.length)assert.match(snapshot.errors[0],/BGP: Не опрошены устройства BGP/) + cases.push({phase,endpoint:'compiled snapshot collector',bgpWritten:snapshot.bgpWritten,greWritten:1,sourceErrors:snapshot.errors.length}) + } + assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true +}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/bgp216-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/bgp216-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1 diff --git a/backend/src/routes/bgp-catalog.test.ts b/backend/src/routes/bgp-catalog.test.ts index b06065b6..4d34d9ac 100644 --- a/backend/src/routes/bgp-catalog.test.ts +++ b/backend/src/routes/bgp-catalog.test.ts @@ -39,7 +39,8 @@ const catalog = await app.inject({ method: "GET", url: "/api/bgp/catalog" }) assert.equal(catalog.statusCode, 200) assert.deepEqual(catalog.json(), mixed) const legacy = await app.inject({ method: "GET", url: "/api/bgp/sessions" }) -assert.deepEqual(legacy.json(), [row]) +assert.equal(legacy.statusCode, 503) +assert.deepEqual(legacy.json().failures, mixed.failures) const selected = await app.inject({ method: "GET", url: "/api/servers/2/bgp/sessions" }) assert.equal(selected.statusCode, 503) assert.equal(selected.json().error, "Таймаут BGP") diff --git a/backend/src/routes/bgp.ts b/backend/src/routes/bgp.ts index 27b83615..2fcf2c1a 100644 --- a/backend/src/routes/bgp.ts +++ b/backend/src/routes/bgp.ts @@ -51,6 +51,9 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti app.get("/bgp/sessions", async (_req, reply) => { const devices = await dependencies.listEnabledServers() const catalog = await pollBgpCatalog(devices, dependencies.readSessions) + if (catalog.failures.length) return reply.status(503).send({ + error: "Не удалось опросить все устройства BGP", failures: catalog.failures, + }) return reply.send(catalog.sessions) }) diff --git a/backend/src/services/bgp-peers-live.test.ts b/backend/src/services/bgp-peers-live.test.ts new file mode 100644 index 00000000..1b458f06 --- /dev/null +++ b/backend/src/services/bgp-peers-live.test.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict' +import {fetchBgpSessionsForAlerts} from './bgp-peers-live.js' +import {collectGreBgpSnapshotOnce} from './gre-bgp-snapshot-collector.js' +import {servers} from '../db/schema.js' +import type {BgpSessionRead} from '../types/server.js' +const devices=[{id:1,name:'Первый'},{id:2,name:'Второй'}] as typeof servers.$inferSelect[] +let calls=0 +for(const phase of ['populated','empty','partial','failed','disabled','malformed'] as const){ + const deps={listEnabledServers:async()=>phase==='disabled'?[]:devices,readSessions:async(server:typeof servers.$inferSelect)=>{calls++;if(phase==='failed'||(phase==='partial'&&server.id===2))throw Error('native401');if(phase==='malformed')return [{id:'*1',serverId:99}] as BgpSessionRead[];return phase==='populated'?[{id:'*1',serverId:server.id,serverName:server.name,name:'own-peer',state:'Established'} as BgpSessionRead]:[]}} + const before=calls,result=await collectGreBgpSnapshotOnce({fetchGre:async()=>[{targetLabel:'GRE-control',status:'up'}],fetchBgp:()=>fetchBgpSessionsForAlerts(deps)}) + assert.equal(result.greWritten,1);assert.equal(result.bgpWritten,phase==='populated'?2:0);assert.equal(result.errors.length,['partial','failed','malformed'].includes(phase)?1:0) + if(result.errors.length)assert.match(result.errors[0],/BGP: Не опрошены устройства BGP/) + if(phase==='disabled')assert.equal(calls,before) +} +console.log('BGP alert observations: populated/empty/partial/failed/disabled/malformed; source errors recorded, GRE preserved') diff --git a/backend/src/services/bgp-peers-live.ts b/backend/src/services/bgp-peers-live.ts index cf7800e2..08caa51f 100644 --- a/backend/src/services/bgp-peers-live.ts +++ b/backend/src/services/bgp-peers-live.ts @@ -14,21 +14,21 @@ export function bgpPeerAlertKey(s: Pick): return `${sn}: ${nm}`.trim() } -/** Агрегат BGP-сессий с включённых серверов — как `/api/bgp/sessions`. */ -export async function fetchBgpSessionsForAlerts(): Promise { - const allServers = await db.select().from(servers).where(eq(servers.enabled, true)) - const results = await Promise.all( - allServers.map(async (server) => { - try { - const client = MikrotikClient.fromServer(server) - const raw = await client.getBgpSessions() - return parseBgpSessions(server, raw) - } catch { - return [] as BgpSessionRead[] - } - }), - ) - return results.flat() +interface BgpAlertDependencies { + listEnabledServers: () => Promise> + readSessions: (server: typeof servers.$inferSelect) => Promise +} +const alertDefaults: BgpAlertDependencies = { + listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)), + readSessions: async server => parseBgpSessions(server, await MikrotikClient.fromServer(server).getBgpSessions()), +} + +/** An incomplete poll is an observation error, never a confirmed empty snapshot. */ +export async function fetchBgpSessionsForAlerts(dependencies: BgpAlertDependencies = alertDefaults): Promise { + const catalog = await pollBgpCatalog(await dependencies.listEnabledServers(), dependencies.readSessions) + if (catalog.failures.length) throw new Error("Не опрошены устройства BGP: " + + catalog.failures.map(failure => `${failure.serverName}: ${failure.error}`).join("; ")) + return catalog.sessions } export async function countBgpSessions():Promise{ diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index e59cbf17..4e9ab420 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. 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). @@ -2587,3 +2587,7 @@ Production compiled SQL/API29 на собственной ограниченно ## 12.5.215 — OSPF/BFD (2026-10-14) Пять legacy endpoints возвращают503 при неполном опросе. Успешные envelopes сохранены; SQL25/localHTTP25 и live API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md#125215--явные-ошибки-legacy-ospfbfd-api-2026-10-14). Backend ux-ospf215, frontend ux-gre214; следующий216 — legacy BGP. + +## 12.5.216 — BGP snapshots (2026-10-14) + +Неполный BGP poll больше не считается успешным пустым ответом/снимком. GRE при ошибке BGP сохраняется. Production17 и реальный API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md). Backend ux-bgp216, frontend ux-gre214; следующий217 — WAN runtime. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 0b8a7715..24c27c04 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 8d36a968..b66f2328 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,16 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. | +## 12.5.216 — неполный BGP опрос в legacy API и snapshots (2026-10-14) + +Legacy /api/bgp/sessions возвращал sessions, отбрасывая failures. fetchBgpSessionsForAlerts также подавлял native errors через пустые массивы, и collector отмечал такой снимок без ошибки источника. Теперь endpoint возвращает503+failures, а источник snapshots отвергает неполный опрос. Collector записывает BGP error, сохраняет независимо прочитанный GRE; права, rules/evaluation и формат успешного массива не менялись. /bgp/catalog сохраняет отдельный контракт частичных результатов. + +Пять профильных скриптов прошли: catalog HTTP, alert source (populated/empty/partial/failed/disabled/malformed), collector, history и guards сигналов. Backend build прошёл. Baseline ux-ospf215 и production ux-bgp216: по17 SQL/native-fixture проверок, включая anonymous401/wrong permission403 до native reads, пять фаз endpoint/catalog/compiled collector; native writes0 и disabled Home Router calls0. Первая fixture попытка использовала mm:network:read и вернула403: failure сохранён, fixture получил необходимое mm:bgp:read, отдельно оставлена проверка отказа неправильной роли. Все собственные DB/roles удалены. + +Backend ux-bgp216 healthy: compiled1052/source483 matched; frontend ux-gre214 сохранён. Portal JWT: legacy/catalog GET200, anonymous401, native resource200 на обоих CHR, real BGP UI1440/320 без page errors/mutations. Сетевые настройки и пароли не менялись; Telegram не отправлялся. Ошибки вводились только в отдельном native HTTP fixture, физическая потеря WAN и полный alert delivery pipeline этим блоком не принимаются. + +Протоколы: [baseline](ux-ui/acceptance/bgp216-baseline.json), [fixed](ux-ui/acceptance/bgp216-fixed.json), [initial fixture failure](ux-ui/acceptance/bgp216-initial-fixture-failure.json), [build](ux-ui/acceptance/bgp216-build-proof-2026-10-14.json), [deployment](ux-ui/acceptance/bgp216-deployment-2026-10-14.json), [live](ux-ui/acceptance/bgp216-live-2026-10-14.json). Следующий217 — legacy WAN runtime: подавление ошибок чтения и опрос выключенного устройства. + ## 12.5.215 — явные ошибки legacy OSPF/BFD API (2026-10-14) Пять legacy GET (/ospf/neighbors, /interfaces, /instances, /all и /bfd/sessions) раньше возвращали200 с пустыми/неполными данными при отказе RouterOS. Теперь любой неудачный опрос даёт503 с serverId/serverName/error. Успешные массивы и пять ключей /ospf/all сохранены; правила parser defaults, права и сетевые операции не менялись. Активный frontend использует /ospf/catalog с отдельным контрактом частичных результатов; он сохранён. diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index 7bfbaad8..a9b837ca 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,10 +1,10 @@ # Оставшаяся внешняя приёмка — 2026-10-13 -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. -Это актуальная матрица после 12.5.215. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.216. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-ospf215`, frontend `ux-gre214`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-bgp216`, frontend `ux-gre214`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -1505,3 +1505,7 @@ Production compiled SQL/API29 на собственной ограниченно ## 12.5.215 — OSPF/BFD (2026-10-14) Пять legacy endpoints возвращают503 при неполном опросе. Успешные envelopes сохранены; SQL25/localHTTP25 и live API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md#125215--явные-ошибки-legacy-ospfbfd-api-2026-10-14). Backend ux-ospf215, frontend ux-gre214; следующий216 — legacy BGP. + +## 12.5.216 — BGP snapshots (2026-10-14) + +Неполный BGP poll больше не считается успешным пустым ответом/снимком. GRE при ошибке BGP сохраняется. Production17 и реальный API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md). Backend ux-bgp216, frontend ux-gre214; следующий217 — WAN runtime. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 5b2d2a77..f3596e35 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -3932,3 +3932,14 @@ Backend ux-ospf215: compiled1052 и source481 matched, healthy; frontend ux-gre2 Протоколы: [baseline](ux-ui/acceptance/ospf215-baseline.json), [production SQL](ux-ui/acceptance/ospf215-fixed.json), [initial fixture failure](ux-ui/acceptance/ospf215-initial-fixture-failure.json), [build proof](ux-ui/acceptance/ospf215-build-proof-2026-10-14.json), [deployment](ux-ui/acceptance/ospf215-deployment-2026-10-14.json), [live](ux-ui/acceptance/ospf215-live-2026-10-14.json). Следующий216 — legacy BGP sessions для alerts: каталог содержит failures, но старый endpoint их отбрасывает. + +## 12.5.216 — неполный BGP опрос в legacy API и snapshots (2026-10-14) + +Legacy /api/bgp/sessions возвращал sessions, отбрасывая failures. fetchBgpSessionsForAlerts также подавлял native errors через пустые массивы, и collector отмечал такой снимок без ошибки источника. Теперь endpoint возвращает503+failures, а источник snapshots отвергает неполный опрос. Collector записывает BGP error, сохраняет независимо прочитанный GRE; права, rules/evaluation и формат успешного массива не менялись. /bgp/catalog сохраняет отдельный контракт частичных результатов. + +Пять профильных скриптов прошли: catalog HTTP, alert source (populated/empty/partial/failed/disabled/malformed), collector, history и guards сигналов. Backend build прошёл. Baseline ux-ospf215 и production ux-bgp216: по17 SQL/native-fixture проверок, включая anonymous401/wrong permission403 до native reads, пять фаз endpoint/catalog/compiled collector; native writes0 и disabled Home Router calls0. Первая fixture попытка использовала mm:network:read и вернула403: failure сохранён, fixture получил необходимое mm:bgp:read, отдельно оставлена проверка отказа неправильной роли. Все собственные DB/roles удалены. + +Backend ux-bgp216 healthy: compiled1052/source483 matched; frontend ux-gre214 сохранён. Portal JWT: legacy/catalog GET200, anonymous401, native resource200 на обоих CHR, real BGP UI1440/320 без page errors/mutations. Сетевые настройки и пароли не менялись; Telegram не отправлялся. Ошибки вводились только в отдельном native HTTP fixture, физическая потеря WAN и полный alert delivery pipeline этим блоком не принимаются. + +Протоколы: [baseline](ux-ui/acceptance/bgp216-baseline.json), [fixed](ux-ui/acceptance/bgp216-fixed.json), [initial fixture failure](ux-ui/acceptance/bgp216-initial-fixture-failure.json), [build](ux-ui/acceptance/bgp216-build-proof-2026-10-14.json), [deployment](ux-ui/acceptance/bgp216-deployment-2026-10-14.json), [live](ux-ui/acceptance/bgp216-live-2026-10-14.json). Следующий217 — legacy WAN runtime: подавление ошибок чтения и опрос выключенного устройства. + diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index e87ba0c9..4c14710e 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Актуальное продолжение 2026-10-14: блок215 завершён. Пять legacy OSPF/BFD GET больше не скрывают частичные/полные отказы за200. Успешные envelopes сохранены. Local HTTP25, isolated production SQL25, profile14+5; compiled1052/source481 matched. Real Portal API6/anonymous6, native resource2 и OSPF UI1440/320 прошли. Own DB/roles удалены; CHR пароли не менялись. Backend ux-ospf215, frontend ux-gre214. Следующий216 — legacy BGP GET для оповещений. Этап12.5 открыт. +Актуальное продолжение 2026-10-14: блок216 завершён. Legacy BGP GET и реальный источник BGP snapshots больше не скрывают неполный опрос. Ошибка наблюдения записывается в snapshot; GRE сохраняется. Пять профильных скриптов, SQL/native17 и live API2/anonymous2/native2/UI1440/320 прошли. Backend ux-bgp216/compiled1052/source483 matched, frontend ux-gre214; собственные DB/roles удалены, пароли CHR не менялись. Следующий217 — WAN runtime: скрытые read errors и disabled no-poll. Этап12.5 открыт. | Параметр | Проверенное состояние | | --- | --- | @@ -2318,3 +2318,7 @@ Production compiled SQL/API29 на собственной ограниченно ## 12.5.215 — OSPF/BFD (2026-10-14) Пять legacy endpoints возвращают503 при неполном опросе. Успешные envelopes сохранены; SQL25/localHTTP25 и live API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md#125215--явные-ошибки-legacy-ospfbfd-api-2026-10-14). Backend ux-ospf215, frontend ux-gre214; следующий216 — legacy BGP. + +## 12.5.216 — BGP snapshots (2026-10-14) + +Неполный BGP poll больше не считается успешным пустым ответом/снимком. GRE при ошибке BGP сохраняется. Production17 и реальный API/UI приняты. Подробности и границы — [журнал](ux-ui-progress.md). Backend ux-bgp216, frontend ux-gre214; следующий217 — WAN runtime. diff --git a/docs/ux-ui/acceptance/bgp216-baseline.json b/docs/ux-ui/acceptance/bgp216-baseline.json new file mode 100644 index 00000000..09327661 --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-baseline.json @@ -0,0 +1,130 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "baseline": true, + "database": "mm_accept_166_1791913047176", + "cases": [ + { + "phase": "anonymous", + "endpoint": "bgp/sessions", + "status": 401, + "nativeCalls": 0 + }, + { + "phase": "wrong permission", + "endpoint": "bgp/sessions", + "status": 403, + "nativeCalls": 0 + }, + { + "phase": "populated", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "populated", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "populated", + "endpoint": "compiled snapshot collector", + "bgpWritten": 2, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "empty", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "empty", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "empty", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "partial", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "partial", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 1, + "nativeCalls": 2 + }, + { + "phase": "partial", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "failed", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "failed", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 2, + "nativeCalls": 2 + }, + { + "phase": "failed", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "disabled", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 0 + }, + { + "phase": "disabled", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 0 + }, + { + "phase": "disabled", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + } + ], + "scope": "Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged", + "passed": true, + "nativeWrites": 0, + "disabledHomeRouterCalls": 0, + "ownedDatabaseAndRoleRemoved": true +} diff --git a/docs/ux-ui/acceptance/bgp216-build-proof-2026-10-14.json b/docs/ux-ui/acceptance/bgp216-build-proof-2026-10-14.json new file mode 100644 index 00000000..ee9c52e7 --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-build-proof-2026-10-14.json @@ -0,0 +1,24 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "compiled": { + "matched": 1052, + "missing": 0, + "different": 0 + }, + "source": [ + { + "kind": "backend", + "matched": 483 + } + ], + "profileScripts": [ + "bgp-catalog", + "bgp-peers-live", + "gre-bgp-snapshot-collector", + "gre-bgp-snapshot-history", + "collector-source-signals" + ], + "sourcePhases": 6, + "productionCases": 17 +} diff --git a/docs/ux-ui/acceptance/bgp216-deployment-2026-10-14.json b/docs/ux-ui/acceptance/bgp216-deployment-2026-10-14.json new file mode 100644 index 00000000..c742d48c --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-deployment-2026-10-14.json @@ -0,0 +1,21 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "backendSourceMatched": 483, + "compiledMatched": 1052, + "frontendChanged": false, + "services": { + "backend": { + "image": "mmapp-test/backend:ux-bgp216", + "imageId": "sha256:a479c4dfc6773cf19b6882b49ccf53b5ff92dcb44ebecb1cf223047bf59c2987", + "running": true, + "health": "healthy" + }, + "frontend": { + "image": "mmapp-test/frontend:ux-gre214", + "imageId": "sha256:7035e5ba628276a9927cce4814739dadd920dba0606e3b9fc316cc0262f77d5f", + "running": true, + "health": null + } + } +} diff --git a/docs/ux-ui/acceptance/bgp216-fixed.json b/docs/ux-ui/acceptance/bgp216-fixed.json new file mode 100644 index 00000000..00221839 --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-fixed.json @@ -0,0 +1,130 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "baseline": false, + "database": "mm_accept_166_1791913132006", + "cases": [ + { + "phase": "anonymous", + "endpoint": "bgp/sessions", + "status": 401, + "nativeCalls": 0 + }, + { + "phase": "wrong permission", + "endpoint": "bgp/sessions", + "status": 403, + "nativeCalls": 0 + }, + { + "phase": "populated", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "populated", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "populated", + "endpoint": "compiled snapshot collector", + "bgpWritten": 2, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "empty", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "empty", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 2 + }, + { + "phase": "empty", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + }, + { + "phase": "partial", + "endpoint": "bgp/sessions", + "status": 503, + "failures": 1, + "nativeCalls": 2 + }, + { + "phase": "partial", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 1, + "nativeCalls": 2 + }, + { + "phase": "partial", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 1 + }, + { + "phase": "failed", + "endpoint": "bgp/sessions", + "status": 503, + "failures": 2, + "nativeCalls": 2 + }, + { + "phase": "failed", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 2, + "nativeCalls": 2 + }, + { + "phase": "failed", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 1 + }, + { + "phase": "disabled", + "endpoint": "bgp/sessions", + "status": 200, + "failures": 0, + "nativeCalls": 0 + }, + { + "phase": "disabled", + "endpoint": "bgp/catalog", + "status": 200, + "failures": 0, + "nativeCalls": 0 + }, + { + "phase": "disabled", + "endpoint": "compiled snapshot collector", + "bgpWritten": 0, + "greWritten": 1, + "sourceErrors": 0 + } + ], + "scope": "Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged", + "passed": true, + "nativeWrites": 0, + "disabledHomeRouterCalls": 0, + "ownedDatabaseAndRoleRemoved": true +} diff --git a/docs/ux-ui/acceptance/bgp216-initial-fixture-failure.json b/docs/ux-ui/acceptance/bgp216-initial-fixture-failure.json new file mode 100644 index 00000000..41137cb1 --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-initial-fixture-failure.json @@ -0,0 +1,12 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "baseline": true, + "database": "mm_accept_166_1791912911021", + "cases": [], + "scope": "Compiled production routes/auth, isolated PostgreSQL and RouterOS HTTP fixture; real CHR unchanged", + "passed": false, + "errorName": "AssertionError", + "nativeWrites": 0, + "disabledHomeRouterCalls": 0 +} diff --git a/docs/ux-ui/acceptance/bgp216-live-2026-10-14.json b/docs/ux-ui/acceptance/bgp216-live-2026-10-14.json new file mode 100644 index 00000000..6e6e3511 --- /dev/null +++ b/docs/ux-ui/acceptance/bgp216-live-2026-10-14.json @@ -0,0 +1,50 @@ +{ + "date": "2026-10-14", + "block": "12.5.216", + "backend": "ux-bgp216", + "frontend": "ux-gre214", + "cases": [ + { + "endpoint": "/api/bgp/sessions", + "status": 200, + "anonymousStatus": 401, + "envelopePreserved": true + }, + { + "endpoint": "/api/bgp/catalog", + "status": 200, + "anonymousStatus": 401, + "envelopePreserved": true + } + ], + "hosts": [ + { + "host": "194.226.121.150", + "nativeResourceStatus": 200, + "passwordUnchanged": true + }, + { + "host": "104.165.244.7", + "nativeResourceStatus": 200, + "passwordUnchanged": true + } + ], + "ui": [ + { + "width": 1440, + "actualCatalogStatus": 200, + "pageErrors": 0, + "mutationRequests": 0 + }, + { + "width": 320, + "actualCatalogStatus": 200, + "pageErrors": 0, + "mutationRequests": 0 + } + ], + "realPortalJWT": true, + "apiMocks": false, + "nativeWrites": 0, + "scope": "Healthy live legacy/catalog GET and active OSPF UI; failures simulated only in isolated native HTTP fixture" +}