From 2b6d3ab2e0cd9d5b692e93c4fb7173628aefc560 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 3 Oct 2026 14:12:06 +0700 Subject: [PATCH] =?UTF-8?q?fix(evolution):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D1=82=D1=8C=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=20=D0=B8=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- .../plugins/auth.evolution-settings.test.ts | 10 +- backend/src/routes/evobgp-catalog.test.ts | 17 +++ backend/src/routes/evobgp-catalog.ts | 126 +++++++++++++++++ backend/src/routes/evobgp.ts | 129 +----------------- docs/ux-ui-plan.md | 2 + docs/ux-ui-progress.md | 10 +- docs/ux-ui-stage-11b.md | 2 + 8 files changed, 169 insertions(+), 129 deletions(-) create mode 100644 backend/src/routes/evobgp-catalog.test.ts create mode 100644 backend/src/routes/evobgp-catalog.ts diff --git a/backend/package.json b/backend/package.json index 6ce1fe9b..c7ec8144 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,7 @@ "test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts", "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:probes && npm run test:exec && 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:scheduler && 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 && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution", - "test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts", + "test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts", "test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts", "test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts", "test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts", diff --git a/backend/src/plugins/auth.evolution-settings.test.ts b/backend/src/plugins/auth.evolution-settings.test.ts index 6998d19e..566b036b 100644 --- a/backend/src/plugins/auth.evolution-settings.test.ts +++ b/backend/src/plugins/auth.evolution-settings.test.ts @@ -1,15 +1,17 @@ import assert from "node:assert/strict" import Fastify from "fastify" import {SignJWT} from "jose" +import {createEvoBgpCatalogRoutes} from "../routes/evobgp-catalog.js" import {createEvoBgpSettingsRoutes} from "../routes/evobgp-settings.js" process.env.AUTH_REQUIRED="true";process.env.AUTH_JWT_SECRET="evolution-fixture-secret";process.env.AUTH_ISSUER="https://auth.test.local";process.env.NODE_ENV="test" const {default:authPlugin,requireAuth}=await import("./auth.js") const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth) -let writes=0,probes=0 +let writes=0,probes=0,catalogs=0 await app.register(createEvoBgpSettingsRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}),write:async()=>{writes++;return {baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}},probe:async()=>{probes++;return {items:[]}}}),{prefix:"/api"}) +await app.register(createEvoBgpCatalogRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:true,apiKey:"fixture-only"}),fetchCatalog:async()=>{catalogs++;return {modules:{items:[]},domains:{items:[]},ip_ranges:{items:[]},asns:{items:[]},communities:{items:[]}}},serversByCommunity:async()=>new Map()}),{prefix:"/api"}) for(const permission of ["mm:settings:admin","mm:settings:write","mm:traffic:admin","mm:dashboard:read"]){ const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("evolution-fixture-secret")) - for(const [method,url] of [["GET","/api/evobgp/settings"],["PUT","/api/evobgp/settings"],["POST","/api/evobgp/test"]] as const){const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:{enabled:false}}:{})});assert.equal(response.statusCode,permission==="mm:settings:admin"?200:403);assert.equal(response.body.includes("fixture-only"),false)} + for(const [method,url] of [["GET","/api/evobgp/settings"],["PUT","/api/evobgp/settings"],["POST","/api/evobgp/test"],["POST","/api/evobgp/catalog"]] as const){const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:{enabled:false}}:{})});assert.equal(response.statusCode,permission==="mm:settings:admin"?200:403);assert.equal(response.body.includes("fixture-only"),false)} } -assert.equal(writes,1);assert.equal(probes,1);assert.equal((await app.inject({method:"GET",url:"/api/evobgp/settings"})).statusCode,401) -await app.close();console.log("Evolution JWT gate: actual settings/test handlers require settings admin, no key echo") +assert.equal(writes,1);assert.equal(probes,1);assert.equal(catalogs,1);assert.equal((await app.inject({method:"POST",url:"/api/evobgp/catalog"})).statusCode,401);assert.equal((await app.inject({method:"GET",url:"/api/evobgp/settings"})).statusCode,401) +await app.close();console.log("Evolution JWT gate: actual settings/test/catalog handlers require settings admin, no key echo") diff --git a/backend/src/routes/evobgp-catalog.test.ts b/backend/src/routes/evobgp-catalog.test.ts new file mode 100644 index 00000000..98e31e05 --- /dev/null +++ b/backend/src/routes/evobgp-catalog.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict" +import Fastify from "fastify" +import {createEvoBgpCatalogRoutes} from "./evobgp-catalog.js" +const empty={modules:{items:[]},domains:{items:[]},ip_ranges:{items:[]},asns:{items:[]},communities:{items:[]}} +let settings={enabled:true,baseUrl:"EVO.EXAMPLE.test/",apiKey:"Bearer fixture-secret"},reads=0,fetches=0,serverReads=0,failFetch=false,failLocal=false +let catalog:unknown=empty +const app=Fastify();await app.register(createEvoBgpCatalogRoutes({read:async()=>{reads++;return {...settings}},fetchCatalog:async(root,key)=>{fetches++;assert.equal(root,"https://evo.example.test");assert.equal(key,"fixture-secret");if(failFetch)throw Error("upstream unavailable");return catalog},serversByCommunity:async()=>{serverReads++;if(failLocal)throw Error("local associations unavailable");return new Map([["64512:1",3]])},now:()=>new Date("2026-10-03T00:00:00Z")}),{prefix:"/api"}) +const load=()=>app.inject({method:"POST",url:"/api/evobgp/catalog",payload:{baseUrl:"https://untrusted.test",apiKey:"untrusted"}}) +let response=await load();assert.equal(response.statusCode,200);assert.deepEqual(response.json(),{fetchedAt:"2026-10-03T00:00:00.000Z",modules:[],domains:[],ipRanges:[],asns:[],communities:[]});assert.equal(reads,1);assert.equal(fetches,1) +catalog={...empty,modules:{items:[{id:"m",name:"Example module",type:"custom"}]},domains:{items:[{module_id:"m",entry:{id:"d",fqdn:"example.test",community_id:"c"}}]},asns:{items:[{module_id:"m",entry:{id:"a",asn:64512,community_id:"c"}},{module_id:"m",entry:{id:"z",asn:64513,community_id:"z",prefix_count:0}}]},communities:{items:[{id:"c",community:"64512:1",title:"Example community"},{id:"z",community:"64512:2"},{id:"empty",community:"64512:3"}]}} +response=await load();assert.equal(response.statusCode,200);const mapped=response.json();assert.equal(mapped.asns[0].prefixes,null);assert.equal(mapped.asns[1].prefixes,0);assert.equal(mapped.communities[0].prefixCount,null);assert.equal(mapped.communities[0].serverCount,3);assert.equal(mapped.communities[1].prefixCount,0);assert.equal(mapped.communities[2].prefixCount,0);assert.equal(mapped.domains[0].purpose,"Example module");assert.equal(response.body.includes("fixture-secret"),false) +const serverBefore=serverReads;for(const malformed of [{},null,{...empty,domains:undefined},{...empty,asns:{items:[{module_id:"m",entry:{id:"a",asn:1,prefix_count:1.2}}]}}]){catalog=malformed;response=await load();assert.equal(response.statusCode,502);assert.match(response.json().error,/не подтверждён/);assert.equal(response.json().domains,undefined)}assert.equal(serverReads,serverBefore) +settings={...settings,enabled:false};const before=fetches;response=await load();assert.equal(response.statusCode,400);assert.equal(fetches,before) +settings={...settings,enabled:true,apiKey:""};response=await load();assert.equal(response.statusCode,400);assert.equal(fetches,before) +settings={...settings,apiKey:"fixture-secret"};catalog=empty;failFetch=true;response=await load();assert.equal(response.statusCode,502);assert.equal(fetches,before+1);assert.equal(response.json().domains,undefined) +failFetch=false;failLocal=true;response=await load();assert.equal(response.statusCode,502);assert.equal(response.json().domains,undefined) +await app.close();console.log("Evolution catalog HTTP: one settings snapshot, DB credentials only, known empty/nullable counts, malformed/upstream/local errors without fabricated success") diff --git a/backend/src/routes/evobgp-catalog.ts b/backend/src/routes/evobgp-catalog.ts new file mode 100644 index 00000000..8654f559 --- /dev/null +++ b/backend/src/routes/evobgp-catalog.ts @@ -0,0 +1,126 @@ +import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" +import { normalizeEvoBgpBaseUrl as normalizeBaseUrl, normalizeEvoBgpApiKey as normalizeApiKey } from "@mmapp/contracts/evobgp" +import { parseEvoCatalogRaw, aggregateCommunityRouteCounts } from "../services/evobgp-catalog.js" +interface Dependencies { + read: () => Promise<{enabled:boolean;baseUrl:string;apiKey:string}> + fetchCatalog: (root:string,key:string) => Promise + serversByCommunity: () => Promise> + now?: () => Date +} +export function createEvoBgpCatalogRoutes(deps:Dependencies):FastifyPluginAsyncZod { + return async app => { + app.post("/evobgp/catalog", async (_req, reply) => { + const row = await deps.read() + if (!row.enabled) { + return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" }) + } + const root = normalizeBaseUrl(row.baseUrl) + const apiKey = normalizeApiKey(row.apiKey) + const cred = root && apiKey ? {root,apiKey} : null + if (!cred) { + return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" }) + } + try { + const catalog = parseEvoCatalogRaw(await deps.fetchCatalog(cred.root, cred.apiKey)) + const modules = catalog.modules.items + const modMap = new Map(modules.map((m) => [m.id, m])) + const commItems = catalog.communities.items + const commLabel = new Map() + for (const c of commItems) { + const label = (c.community && c.title) + ? `${c.title} (${c.community})` + : (c.title ?? c.community ?? c.id) + commLabel.set(c.id, label) + } + + const now = deps.now?.() ?? new Date() + const fmtTime = now.toLocaleString("ru-RU") + const fetchedAt = now.toISOString() + + const domains = catalog.domains.items.map((rowItem) => { + const mod = modMap.get(rowItem.module_id) + const cid = rowItem.entry.community_id + return { + id: `${rowItem.module_id}:${rowItem.entry.id}`, + domain: rowItem.entry.fqdn, + resolvedIp: "—", + asn: "—", + purpose: mod?.name ?? "—", + filter: cid ? (commLabel.get(cid) ?? cid) : "—", + updated: fmtTime, + enabled: true, + } + }) + + const ipRanges = catalog.ip_ranges.items.map((rowItem) => { + const mod = modMap.get(rowItem.module_id) + const cid = rowItem.entry.community_id + return { + id: `${rowItem.module_id}:${rowItem.entry.id}`, + cidr: rowItem.entry.prefix, + asn: "—", + country: "—", + purpose: mod?.name ?? "—", + filter: commLabel.get(cid) ?? cid, + updated: fmtTime, + enabled: true, + } + }) + + const asns = catalog.asns.items.map((rowItem) => { + const cid = rowItem.entry.community_id + const e = rowItem.entry + const name = (e.asn_name ?? "").trim() + const prefixes = e.prefix_count ?? null + let updated = fmtTime + const rawResolved = (e.asn_resolved_at ?? "").trim() + if (rawResolved) { + const d = new Date(rawResolved) + if (!Number.isNaN(d.getTime())) updated = d.toLocaleString("ru-RU") + } + return { + id: `${rowItem.module_id}:${e.id}`, + asn: `AS${e.asn}`, + org: name || "—", + country: "—", + prefixes, + filter: cid ? (commLabel.get(cid) ?? cid) : "—", + updated, + enabled: true, + } + }) + + const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog) + const serversByCommunityStr = await deps.serversByCommunity() + + const communities = commItems.map((c) => { + const valueStr = (c.community ?? "").trim() + return { + id: c.id, + value: c.community ?? c.id, + name: c.title ?? c.community ?? c.id, + description: "", + type: "custom" as const, + filterIds: [] as string[], + serverCount: valueStr ? (serversByCommunityStr.get(valueStr) ?? 0) : 0, + prefixCount: routeCountsByCommUuid.has(c.id) ? routeCountsByCommUuid.get(c.id)! : 0, + action: "permit" as const, + enabled: true, + } + }) + + return reply.send({ + fetchedAt, + modules: modules.map((m) => ({ id: m.id, name: m.name, type: m.type })), + domains, + ipRanges, + asns, + communities, + }) + } catch (e) { + const msg = e instanceof Error ? e.message : "Ошибка EvoBGP" + return reply.status(502).send({ error: msg }) + } + }) + } +} diff --git a/backend/src/routes/evobgp.ts b/backend/src/routes/evobgp.ts index b0bf6ceb..15f19054 100644 --- a/backend/src/routes/evobgp.ts +++ b/backend/src/routes/evobgp.ts @@ -1,7 +1,6 @@ import { eq, sql } from "drizzle-orm" import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" -import { normalizeEvoBgpBaseUrl as normalizeBaseUrl, normalizeEvoBgpApiKey as normalizeApiKey } from "@mmapp/contracts/evobgp" -import { parseEvoCatalogRaw, aggregateCommunityRouteCounts } from "../services/evobgp-catalog.js" +import { createEvoBgpCatalogRoutes } from "./evobgp-catalog.js" import { createEvoBgpSettingsRoutes } from "./evobgp-settings.js" import { db } from "../db/index.js" import { evobgpSettings, filterRules } from "../db/schema.js" @@ -71,14 +70,6 @@ async function fetchEvoJson(root: string, path: string, token: string): Promi } } -async function credentialsFromDb(): Promise<{ root: string; apiKey: string } | null> { - const row = await ensureEvobgpRow() - const root = normalizeBaseUrl(row.baseUrl) - const apiKey = normalizeApiKey(row.apiKey) - if (!root || !apiKey) return null - return { root, apiKey } -} - const evobgpRoutes: FastifyPluginAsyncZod = async (app) => { await app.register(createEvoBgpSettingsRoutes({ read: ensureEvobgpRow, @@ -95,119 +86,11 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => { }, })) - /** - * Прокси к GET /v1/router-lists/catalog — учётные данные только из БД. - */ - app.post("/evobgp/catalog", async (_req, reply) => { - const row = await ensureEvobgpRow() - if (!row.enabled) { - return reply.status(400).send({ error: "Интеграция EvoBGP выключена в настройках" }) - } - const cred = await credentialsFromDb() - if (!cred) { - return reply.status(400).send({ error: "Не заданы базовый URL или API-ключ в БД" }) - } - try { - const catalog = parseEvoCatalogRaw(await fetchEvoJson(cred.root, "/v1/router-lists/catalog", cred.apiKey)) - const modules = catalog.modules.items - const modMap = new Map(modules.map((m) => [m.id, m])) - const commItems = catalog.communities.items - const commLabel = new Map() - for (const c of commItems) { - const label = (c.community && c.title) - ? `${c.title} (${c.community})` - : (c.title ?? c.community ?? c.id) - commLabel.set(c.id, label) - } - - const fmtTime = new Date().toLocaleString("ru-RU") - const fetchedAt = new Date().toISOString() - - const domains = catalog.domains.items.map((rowItem) => { - const mod = modMap.get(rowItem.module_id) - const cid = rowItem.entry.community_id - return { - id: `${rowItem.module_id}:${rowItem.entry.id}`, - domain: rowItem.entry.fqdn, - resolvedIp: "—", - asn: "—", - purpose: mod?.name ?? "—", - filter: cid ? (commLabel.get(cid) ?? cid) : "—", - updated: fmtTime, - enabled: true, - } - }) - - const ipRanges = catalog.ip_ranges.items.map((rowItem) => { - const mod = modMap.get(rowItem.module_id) - const cid = rowItem.entry.community_id - return { - id: `${rowItem.module_id}:${rowItem.entry.id}`, - cidr: rowItem.entry.prefix, - asn: "—", - country: "—", - purpose: mod?.name ?? "—", - filter: commLabel.get(cid) ?? cid, - updated: fmtTime, - enabled: true, - } - }) - - const asns = catalog.asns.items.map((rowItem) => { - const cid = rowItem.entry.community_id - const e = rowItem.entry - const name = (e.asn_name ?? "").trim() - const prefixes = e.prefix_count ?? null - let updated = fmtTime - const rawResolved = (e.asn_resolved_at ?? "").trim() - if (rawResolved) { - const d = new Date(rawResolved) - if (!Number.isNaN(d.getTime())) updated = d.toLocaleString("ru-RU") - } - return { - id: `${rowItem.module_id}:${e.id}`, - asn: `AS${e.asn}`, - org: name || "—", - country: "—", - prefixes, - filter: cid ? (commLabel.get(cid) ?? cid) : "—", - updated, - enabled: true, - } - }) - - const routeCountsByCommUuid = aggregateCommunityRouteCounts(catalog) - const serversByCommunityStr = await distinctServersByFilterCommunityValue() - - const communities = commItems.map((c) => { - const valueStr = (c.community ?? "").trim() - return { - id: c.id, - value: c.community ?? c.id, - name: c.title ?? c.community ?? c.id, - description: "", - type: "custom" as const, - filterIds: [] as string[], - serverCount: valueStr ? (serversByCommunityStr.get(valueStr) ?? 0) : 0, - prefixCount: routeCountsByCommUuid.has(c.id) ? routeCountsByCommUuid.get(c.id)! : 0, - action: "permit" as const, - enabled: true, - } - }) - - return reply.send({ - fetchedAt, - modules: modules.map((m) => ({ id: m.id, name: m.name, type: m.type })), - domains, - ipRanges, - asns, - communities, - }) - } catch (e) { - const msg = e instanceof Error ? e.message : "Ошибка EvoBGP" - return reply.status(502).send({ error: msg }) - } - }) + await app.register(createEvoBgpCatalogRoutes({ + read: ensureEvobgpRow, + fetchCatalog: (root,key) => fetchEvoJson(root,"/v1/router-lists/catalog",key), + serversByCommunity: distinctServersByFilterCommunityValue, + })) } export default evobgpRoutes diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 8ad39cf2..0bb78f9a 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -282,3 +282,5 @@ REST запрос → ожидание → подтверждённый output / Завершён 11Б.3.5.4.1: отдельная форма Evolution dirty/rights/lock/clear confirmation/source; 17 основных, 8 source и 3 responsive E2E, typecheck/lint/build прошли. 6 снимков, 320/ПК/clear просмотрены. Общие Settings и live consumer mocks ещё не приняты. Далее 11Б.3.5.4.2 — consumers/sidebar unknown/disabled/no fallback. Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution. + +Завершён 11Б.3.5.5: Evolution catalog DI HTTP/JWT, один settings snapshot, backend test/build/lint прошли. Evolution принят локально в границах предыдущих form/provider/source/consumer/adaptive проверок; реальный upstream/PG/Portal/RouterOS не принят. Далее 11Б.3.6.1 — достоверные общие Settings без фиктивных сохранений/API keys/MFA; затем system DB, Alerts/Releases/probes. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 384bf8ab..b9f064e4 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -28,7 +28,7 @@ | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | -| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.4.2; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.5; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,14 @@ +## Блок 11Б.3.5.5 — серверная и локальная приёмка Evolution (2026-10-03) + +- Catalog handler вынесен в DI plugin, production route использует те же функции. Enabled/URL/key читаются одним снимком настроек (раньше двумя GET БД), credentials поступают только из сохранённых данных, request body не переопределяет URL/key. Mapping сохраняет null/0 и local server associations; timestamps берутся из одного now. +- HTTP injection проверил known empty, реальный mapping modules/domain/ASN/community, unknown/zero/known sums, malformed upstream (502, без empty receipt и без чтения local associations), disabled/missing key (400, upstream не вызывается), upstream и local-association failures. JWT реального auth plugin и реальных catalog/settings/test handlers: admin 200, остальные 403 до upstream, без токена 401, ключ не возвращается. +- Backend Evolution parser/HTTP/settings/JWT, backend build и lint прошли; prefer-const в новом тесте исправлен. Frontend 17 form/provider, 8 source, 18 consumer и 3 responsive сценария, count E2E, typecheck/production build и снимки описаны в предыдущих блоках; повторять без нового frontend изменения не требуется. +- Evolution принят в границах локальных HTTP-фикстур и DI. Реальный upstream format/version и nullable counts, PG selective writes/associations, Portal, доступность внешнего EvoBGP и фактическое применение сетевых списков требуют тестового окружения. Read-only catalog POST может продолжать работу backend после browser abort; автоматического повтора mutations нет. +- Следующий блок 11Б.3.6.1 — общие Settings: убрать фиктивное сохранение/demo API keys/MFA и параметры без реализации; оставить реальные source/интеграции/переходы, отдельная приёмка system DB операций затем. Alerts/Releases/probe scheduling/приёмка 11Б остаются впереди. + ## Блок 11Б.3.5.4.2 — live потребители каталога (2026-10-03) - Domains/IP ranges/ASN/communities используют mock rows только в mock mode. В live unknown/disabled/no access не становится демонстрационными строками или подтверждённым нулём. Общий status разделяет unread/disabled/error/last snapshot/freshness; retry может перечитать настройки отключённой интеграции. Подтверждённый empty catalogue и search no-results имеют отдельные подсказки. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 8b316fea..17453ac0 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -214,3 +214,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S | 11Б.3.5.4.1 Evolution | [Settings 16](https://reui.io/preview/base/settings-16?ref=mcp) | Статус и свежесть → поля → сохранить / проверить → подтверждённый результат; удаление ключа → подтверждение → результат | Завершён 11Б.3.5.4.2: live catalog consumers/sidebar/Filters без demo fallback, unknown/disabled/last snapshot и current details; 25 E2E, typecheck/профильный lint/build прошли. 3 community screenshots, 320/ПК просмотрены. Sidebar sync-effect baseline error и Filters unused warnings описаны в журнале. Далее 11Б.3.5.5 — HTTP/JWT catalog и приёмка Evolution. + +Завершён 11Б.3.5.5: Evolution catalog DI HTTP/JWT, один settings snapshot, backend test/build/lint прошли. Evolution принят локально в границах предыдущих form/provider/source/consumer/adaptive проверок; реальный upstream/PG/Portal/RouterOS не принят. Далее 11Б.3.6.1 — достоверные общие Settings без фиктивных сохранений/API keys/MFA; затем system DB, Alerts/Releases/probes.