:null}
+ {error?{snapshot?"Каталог не обновлён; показаны последние данные":"Каталог EvoBGP недоступен"}{[evo.settingsError,evo.catalogError].filter(Boolean).join("; ")}:null}
+ {allowed?
Настройки → EvoBGP
:null}
+
+}
+export function EvoBgpCatalogUnavailable(){return }
diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md
index c5c3703f..8ad39cf2 100644
--- a/docs/ux-ui-plan.md
+++ b/docs/ux-ui-plan.md
@@ -280,3 +280,5 @@ REST запрос → ожидание → подтверждённый output /
Завершён 11Б.3.5.3.2: strict upstream catalog, nullable ASN/community counts, unknown «—» вместо 0. Parser/aggregation/HTTP/JWT/build/typecheck/lint и provider/counts E2E прошли; locator inline details исправлен. Реальный Evolution/PG не принят. Далее 11Б.3.5.4.1 — форма Evolution dirty/rights/lock/key clear/source.
Завершён 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.
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md
index 59c07e12..384bf8ab 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.1; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings |
+| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.4.2; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,15 @@
+## Блок 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 имеют отдельные подсказки.
+- Не реализованные import/export/add controls скрыты в live; исправлены пустые действия community details. Sidebar badges требуют known enabled/authorized snapshot. Live community suggestions Filters не используют demo names при unavailable integration, ручной ввод сохранён; сетевые правила не менялись.
+- Community details выбираются по ID из текущего snapshot, а не сохраняют устаревший объект; source перемонтирует локальный контекст. Unknown KPI «—»; filter association KPI live «—», поскольку association IDs upstream не подтверждены. Таблица/детали и filters toolbar адаптированы для телефона.
+- 14 первоначальных consumers/counts E2E и итоговые 25 consumers/Filters data/save (18 consumers, 7 Filters) прошли. Общий typecheck, профильный ESLint и production build прошли. Filters имеет 12 прежних unused warnings (лишняя hook dependency убрана); Sidebar сохраняет исходную sync-effect lint error, она не относится к новым catalog badges.
+- 3 снимка communities 1440/390/320, 320/desktop просмотрены. Реальный Evolution/PG/Portal не принят. Действия mock-импорта остаются демонстрационными.
+- Далее 11Б.3.5.5 — DI HTTP/JWT catalog mapping/error/unknown receipt и локальная приёмка Evolution. Затем общие Settings и оставшаяся группа 11Б.
+
## Блок 11Б.3.5.4.1 — форма Evolution (2026-10-03)
- Evolution вынесен из Settings в отдельную форму на существующих Frame/OpsPanel/FormField/FormToggle/AlertDialog. GET обновляет только нетронутые поля; ключ и другие dirty поля сохраняются при обновлении и ошибке. PUT отправляет только dirty fields через общий нормализующий контракт; пустой ключ не удаляет сохранённый. Успех — один inline receipt, без прежнего двойного toast и фиктивного сохранения mock-режима.
diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md
index fbd1a36e..8b316fea 100644
--- a/docs/ux-ui-stage-11b.md
+++ b/docs/ux-ui-stage-11b.md
@@ -212,3 +212,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.
diff --git a/docs/ux-ui/screens/evolution-communities-1440.png b/docs/ux-ui/screens/evolution-communities-1440.png
new file mode 100644
index 00000000..fd8c6530
Binary files /dev/null and b/docs/ux-ui/screens/evolution-communities-1440.png differ
diff --git a/docs/ux-ui/screens/evolution-communities-320.png b/docs/ux-ui/screens/evolution-communities-320.png
new file mode 100644
index 00000000..f1339847
Binary files /dev/null and b/docs/ux-ui/screens/evolution-communities-320.png differ
diff --git a/docs/ux-ui/screens/evolution-communities-390.png b/docs/ux-ui/screens/evolution-communities-390.png
new file mode 100644
index 00000000..71960fdb
Binary files /dev/null and b/docs/ux-ui/screens/evolution-communities-390.png differ
diff --git a/tests/e2e/evobgp-consumers.spec.ts b/tests/e2e/evobgp-consumers.spec.ts
new file mode 100644
index 00000000..d9a27b01
--- /dev/null
+++ b/tests/e2e/evobgp-consumers.spec.ts
@@ -0,0 +1,28 @@
+import {test,expect,type Page} from "@playwright/test"
+import {evoSettings,evoCatalog} from "./fixtures/evobgp"
+async function setup(page:Page,settings:unknown=evoSettings,catalog:unknown=evoCatalog){await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;return r.fulfill(path==="/api/auth/config"?{json:{required:false}}:path==="/api/evobgp/settings"?{json:settings}:path==="/api/evobgp/catalog"?{json:catalog}:{status:503,json:{error:"outside fixture"}})})}
+const cases=[{path:"domains",mock:"youtube.com"},{path:"ip-ranges",mock:"8.8.8.0/24"},{path:"asns",mock:"AS15169"},{path:"communities",mock:"youtube-bypass"}]
+for(const {path,mock} of cases)for(const state of ["unknown","disabled"] as const)test("Evolution catalog: "+path+"/"+state+" never shows mock rows or confirmed zero",async({page})=>{
+ await setup(page,state==="unknown"?{}:{...evoSettings,enabled:false});await page.goto("/"+path)
+ await expect(page.getByText(state==="unknown"?"Настройки EvoBGP неизвестны":"Интеграция EvoBGP выключена",{exact:true})).toBeVisible()
+ await expect(page.getByText("Данные каталога не получены",{exact:true})).toBeVisible()
+ await expect(page.getByText(mock,{exact:true})).toHaveCount(0)
+ await expect(page.getByRole("row")).toHaveCount(0)
+ if(path!=="communities")await expect(page.getByText("Количество: —",{exact:true})).toBeVisible()
+ await expect(page.getByRole("button",{name:/^Добавить/})).toHaveCount(0)
+})
+test("Evolution catalog: known empty differs from unread and permits refresh",async({page})=>{await setup(page,evoSettings,{...evoCatalog,domains:[]});await page.goto("/domains");await expect(page.getByText("Нет доменов",{exact:true})).toBeVisible();await expect(page.getByText("0 доменов",{exact:true})).toBeVisible();await expect(page.getByText("В подтверждённом каталоге нет записей этого типа",{exact:true})).toBeVisible();await expect(page.getByText("Данные каталога не получены",{exact:true})).toHaveCount(0)})
+test("Evolution catalog: malformed initial response stays unread",async({page})=>{await setup(page,evoSettings,{});await page.goto("/domains");await expect(page.getByText("Каталог EvoBGP недоступен",{exact:true})).toBeVisible();await expect(page.getByText("Данные каталога не получены",{exact:true})).toBeVisible();await expect(page.getByText("0 доменов",{exact:true})).toHaveCount(0)})
+test("Evolution catalog: refresh error preserves search, rows and freshness",async({page})=>{await setup(page);let fail=false;await page.route("**/api/evobgp/catalog",r=>r.fulfill(fail?{status:503,json:{error:"catalog down"}}:{json:evoCatalog}));await page.goto("/domains");await expect(page.getByText("kept.example.test",{exact:true})).toBeVisible();const stamp=page.getByRole("status").filter({hasText:/^Каталог EvoBGP получен:/});const before=await stamp.textContent();await page.getByRole("textbox",{name:"Поиск по домену…"}).fill("kept");fail=true;await page.getByRole("button",{name:"Обновить каталог EvoBGP",exact:true}).click();await expect(page.getByText("Каталог не обновлён; показаны последние данные",{exact:true})).toBeVisible();await expect(page.getByText("kept.example.test",{exact:true})).toBeVisible();await expect(stamp).toHaveText(before!);await expect(page.getByRole("textbox",{name:"Поиск по домену…"})).toHaveValue("kept")})
+test("Evolution catalog: disabled can reload settings to discover enabled integration",async({page})=>{let enabled=false;await setup(page);await page.route("**/api/evobgp/settings",r=>r.fulfill({json:{...evoSettings,enabled}}));await page.goto("/domains");await expect(page.getByText("Интеграция EvoBGP выключена",{exact:true})).toBeVisible();enabled=true;await page.getByRole("button",{name:"Обновить каталог EvoBGP",exact:true}).click();await expect(page.getByText("kept.example.test",{exact:true})).toBeVisible()})
+test("Evolution catalog: mock mode is explicit and does not read integration",async({page})=>{await page.addInitScript(()=>localStorage.setItem("routerlists:data-source","mock"));await setup(page);let requests=0;page.on("request",r=>{if(new URL(r.url()).pathname.startsWith("/api/evobgp/"))requests++});await page.goto("/asns");await expect(page.getByText("Демонстрационный каталог",{exact:true})).toBeVisible();await expect(page.getByText("AS15169",{exact:true})).toBeVisible();expect(requests).toBe(0)})
+test("Evolution catalog: reader without admin cannot load integration or see demo data",async({page})=>{
+ const token="header."+Buffer.from(JSON.stringify({sub:"reader",apps:["mm"],permissions:["mm:dashboard:read","mm:filters:read"],exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture"
+ await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);await setup(page);await page.route("**/api/auth/config",r=>r.fulfill({json:{required:true}}));let reads=0;page.on("request",r=>{if(new URL(r.url()).pathname.startsWith("/api/evobgp/"))reads++});await page.goto("/domains");await expect(page.getByText("Нет доступа к каталогу EvoBGP",{exact:true})).toBeVisible();await expect(page.getByRole("row")).toHaveCount(0);await expect(page.getByRole("button",{name:"Обновить каталог EvoBGP",exact:true})).toHaveCount(0);expect(reads).toBe(0)
+})
+test("Evolution catalog: selected community details follow refreshed snapshot",async({page})=>{
+ const community={id:"c",value:"64512:1",name:"Selected community",description:"",type:"custom",filterIds:[],serverCount:0,prefixCount:4,action:"permit",enabled:true};let prefixes=4;await setup(page);await page.route("**/api/evobgp/catalog",r=>r.fulfill({json:{...evoCatalog,communities:[{...community,prefixCount:prefixes}]}}));await page.goto("/communities");await page.getByRole("row").filter({hasText:"Selected community"}).click();const detail=page.getByText("Маршрутов",{exact:true}).locator("..");await expect(detail.getByText("4",{exact:true})).toBeVisible();prefixes=9;await page.getByRole("button",{name:"Обновить каталог EvoBGP",exact:true}).click();await expect(detail.getByText("9",{exact:true})).toBeVisible();await expect(detail.getByText("4",{exact:true})).toHaveCount(0)
+})
+for(const width of [1440,390,320])test("Evolution catalog: communities adaptive/"+width,async({page})=>{
+ await page.setViewportSize({width,height:1000});await setup(page,evoSettings,{});await page.goto("/communities");await expect(page.getByText("Данные каталога не получены",{exact:true})).toBeVisible();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true);await page.screenshot({path:"docs/ux-ui/screens/evolution-communities-"+width+".png",fullPage:true,animations:"disabled"})
+})