From a8a3eb59fb9c7932d0112f474fe5215acc0de2ca Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 3 Oct 2026 17:34:33 +0700 Subject: [PATCH] =?UTF-8?q?fix(alerts):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=8F=D1=82=D1=8C=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20=D1=87=D0=B0=D1=81=D1=82=D0=B8=D1=87=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D1=81=D0=B1=D0=BE=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-plan.md | 4 +++- docs/ux-ui-progress.md | 9 ++++++++- docs/ux-ui-stage-11b.md | 2 ++ shared/api/alerts-targets.test.ts | 8 ++++++++ shared/api/alerts-targets.ts | 17 +++++++++++++++++ 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 shared/api/alerts-targets.test.ts create mode 100644 shared/api/alerts-targets.ts diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 9db141b1..2e62e81f 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-03 -Этап 11Б в работе; последний проверенный блок 11Б.4.3.1.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.3.1.2: независимые Alerts BGP/GRE readers/last good metadata/source ABA; затем Telegram/rules drafts/locks/rights/unknown/adaptive и GET/revision/outbox audit. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12. +Этап 11Б в работе; последний проверенный блок 11Б.4.3.1.2.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.3.1.2.2: подключить независимые Alerts BGP/GRE readers/status/last good metadata/source ABA; затем Telegram/rules drafts/locks/rights/unknown/adaptive и GET/revision/outbox audit. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). @@ -316,3 +316,5 @@ REST запрос → ожидание → подтверждённый output / 2026-10-03: 11Б.4.2.2.1.2 — transaction rules receipt/readback/lock, DI/API/JWT/build/typecheck/lint прошли. Реальный PG rollback/overlap и optimistic revision не приняты. Далее Alerts UI 11Б.4.3.1. 2026-10-03: 11Б.4.3.1.1 — live Alerts без первоначальных demo/0, strict primary reader/known/time, 3 E2E/lint/typecheck/build прошли. Далее independent secondary readers/source 11Б.4.3.1.2. + +2026-10-03: 11Б.4.3.1.2.1 — strict secondary API (GRE partial failures/unknown status), 4 unit/typecheck/lint прошли. Module не подключён. Далее 11Б.4.3.1.2.2 independent UI/source. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index c17d07ef..ff212b16 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Б.4.3.1.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.3.1.2.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,13 @@ +## Блок 11Б.4.3.1.2.1 — строгие secondary catalog readers (2026-10-03) + +- Новый API module отдельно читает BGP sessions, server labels и GRE для Alerts: schema validation/known empty/caller abort до GET и после decode/30s signal. Отсутствующий GRE status остаётся undefined, не down. GRE partial device failures сохраняются, отсутствие failures в legacy DTO не подменяется [] как доказательство полноты. +- 4 API unit группы, typecheck без incremental и профильный lint прошли. Module пока не подключён к UI; текущий load всё ещё последовательный и маскирует вторичные failures. Source E2E/dirty/form acceptance впереди. +- По реальному backend коду /api/bgp/sessions возвращает только catalog.sessions, без failures: успешный пустой array не доказывает, что все устройства ответили. /filters/gre-tunnels отдаёт failures при aggregate poll; новый reader сохраняет их. Реальные опросы не запускались. +- Следующий точный шаг 11Б.4.3.1.2.2: подключить независимые secondary readers/status/error/time/last good metadata; разрешить primary refresh независимо от задержанных каталогов; source/ABA browser tests. Затем Telegram/rules drafts/locks/rights/unknown/adaptive, GET/revision/outbox audit и оставшиеся 11Б/12. + ## Блок 11Б.4.3.1.1 — первоначальное состояние Alerts и strict reader (2026-10-03) - Live root больше не инициализируется INIT_RULES/INIT_GROUPS/INIT_HISTORY/INIT_TG. До подтверждённого GET нет demo rows, ложного «Всего: 0» и якобы полученной Telegram конфигурации. Known empty отличается от unknown; основной контент раскрывается только после strict getAlerts. Browser receivedAt показан явно; failed background GET сохраняет последний снимок. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index c75a30c3..bf05c8ba 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -246,3 +246,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S Завершён 11Б.4.2.2.1.2: transactional rules/groups receipt/readback и nonblocking replace lock/409; 4 DI + profile/build/typecheck/lint прошли. Real PG и optimistic revision не проверены. Далее 11Б.4.3.1 — Alerts UI source/strict readers/known/error/time; затем drafts/locks/rights/adaptive и GET/outbox audit. Завершён 11Б.4.3.1.1: primary Alerts strict reader/unknown/known empty/time/last snapshot без live initial mocks; 3 E2E/lint/typecheck/build прошли. Secondary readers/dirty/forms пока прежние. Далее 11Б.4.3.1.2 — независимые BGP/GRE/last good/source ABA; затем forms/rights/adaptive/GET/revision/outbox audit. + +Завершён 11Б.4.3.1.2.1: strict BGP/server/GRE target readers, partial GRE failures/unknown status; 4 unit/typecheck/lint прошли, module не подключён. BGP endpoint не раскрывает failures. Далее 11Б.4.3.1.2.2 — independent readers/status/last good/source ABA; затем forms и audit. diff --git a/shared/api/alerts-targets.test.ts b/shared/api/alerts-targets.test.ts new file mode 100644 index 00000000..64801167 --- /dev/null +++ b/shared/api/alerts-targets.test.ts @@ -0,0 +1,8 @@ +import assert from "node:assert/strict" +import {test} from "node:test" +import {getAlertsBgpTargets,getAlertsServerTargets,getAlertsGreTargets} from "./alerts-targets" +test("Alerts target readers preserve known empty collections and unknown optional GRE status",async()=>{const original=globalThis.fetch;try{globalThis.fetch=async()=>Response.json([]);assert.deepEqual(await getAlertsBgpTargets("https://backend.test"),[]);assert.deepEqual(await getAlertsServerTargets("https://backend.test"),[]);globalThis.fetch=async()=>Response.json({tunnels:[]});assert.deepEqual(await getAlertsGreTargets("https://backend.test"),{tunnels:[]});globalThis.fetch=async()=>Response.json({tunnels:[{name:"gre1",serverId:"1"}]});assert.equal((await getAlertsGreTargets("https://backend.test")).tunnels[0].status,undefined)}finally{globalThis.fetch=original}}) +test("Alerts malformed secondary catalogs reject instead of fabricating empty/down",async()=>{const original=globalThis.fetch;try{for(const [call,raw] of [[getAlertsBgpTargets,{}],[getAlertsBgpTargets,[{serverName:"",name:"peer"}]],[getAlertsServerTargets,[{id:"1",name:"server"}]],[getAlertsServerTargets,[{id:1.5,name:"server"}]],[getAlertsGreTargets,{}],[getAlertsGreTargets,{tunnels:null}],[getAlertsGreTargets,{tunnels:[{name:"gre",serverId:""}]}]] as const){globalThis.fetch=async()=>Response.json(raw);await assert.rejects(call("https://backend.test"),/не подтверждён/)}}finally{globalThis.fetch=original}}) +test("Alerts target readers reject pre-abort before GET and use independent bounded signals",async()=>{const original=globalThis.fetch;let calls=0;try{globalThis.fetch=async()=>{calls++;return Response.json([])};const controller=new AbortController();controller.abort();for(const call of [getAlertsBgpTargets,getAlertsServerTargets,getAlertsGreTargets])await assert.rejects(call("https://backend.test",{signal:controller.signal}));assert.equal(calls,0);globalThis.fetch=async(_url,init)=>{assert.ok(init?.signal);return Response.json([])};await getAlertsBgpTargets("https://backend.test")}finally{globalThis.fetch=original}}) + +test("Alerts GRE reader preserves partial device failures instead of treating missing tunnels as complete",async()=>{const original=globalThis.fetch;try{const failures=[{serverId:"1",serverName:"Router",error:"unavailable"}];globalThis.fetch=async()=>Response.json({tunnels:[],failures});assert.deepEqual((await getAlertsGreTargets("https://backend.test")).failures,failures)}finally{globalThis.fetch=original}}) diff --git a/shared/api/alerts-targets.ts b/shared/api/alerts-targets.ts new file mode 100644 index 00000000..433b08dd --- /dev/null +++ b/shared/api/alerts-targets.ts @@ -0,0 +1,17 @@ +import {z} from "zod" +import {requestJson} from "./http-client" +const bgpSchema=z.array(z.object({serverName:z.string().trim().min(1),name:z.string().trim().min(1)})) +const serversSchema=z.array(z.object({id:z.number().int().positive(),name:z.string(),site:z.string().optional(),country:z.string().optional(),host:z.string().optional()})) +const greSchema=z.object({tunnels:z.array(z.object({id:z.string().optional(),name:z.string(),serverId:z.string().min(1),remoteAddress:z.string().optional(),status:z.string().optional(),comment:z.string().optional()})),failures:z.array(z.object({serverId:z.string().min(1),serverName:z.string(),error:z.string()})).optional()}) +type Options={signal?:AbortSignal} +async function read(base:string,path:string,schema:S,message:string,options?:Options):Promise>{ + const signal=options?.signal?AbortSignal.any([options.signal,AbortSignal.timeout(30_000)]):AbortSignal.timeout(30_000) + signal.throwIfAborted() + const parsed=schema.safeParse(await requestJson(base,path,{signal,cache:"no-store"})) + signal.throwIfAborted() + if(!parsed.success)throw Error(message) + return parsed.data +} +export const getAlertsBgpTargets=(base:string,options?:Options)=>read(base,"/api/bgp/sessions",bgpSchema,"Каталог BGP для оповещений не подтверждён",options) +export const getAlertsServerTargets=(base:string,options?:Options)=>read(base,"/api/servers",serversSchema,"Каталог серверов для оповещений не подтверждён",options) +export const getAlertsGreTargets=(base:string,options?:Options)=>read(base,"/api/filters/gre-tunnels",greSchema,"Каталог GRE для оповещений не подтверждён",options)