From 98319c9d5069a259bbcd3386e8712e16911282d0 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 3 Oct 2026 17:06:44 +0700 Subject: [PATCH] =?UTF-8?q?fix(alerts):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D1=8B=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D0=B0=D1=82=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 3 +- backend/src/routes/alerts.ts | 3 +- backend/src/services/alerts-service.ts | 66 +++++-------------- .../src/services/alerts-telegram-send.test.ts | 15 +++++ backend/src/services/alerts-telegram-send.ts | 11 ++++ .../services/alerts-telegram-settings.test.ts | 9 +++ .../src/services/alerts-telegram-settings.ts | 20 ++++++ docs/ux-ui-plan.md | 4 +- docs/ux-ui-progress.md | 9 ++- docs/ux-ui-stage-11b.md | 2 + packages/contracts/src/alerts.ts | 6 +- 11 files changed, 90 insertions(+), 58 deletions(-) create mode 100644 backend/src/services/alerts-telegram-send.test.ts create mode 100644 backend/src/services/alerts-telegram-send.ts create mode 100644 backend/src/services/alerts-telegram-settings.test.ts create mode 100644 backend/src/services/alerts-telegram-settings.ts diff --git a/backend/package.json b/backend/package.json index 2dcbb698..07f2970f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "npm run build -w @mmapp/contracts && tsc", + "test:alerts-telegram": "tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts", "test:alert-engine": "tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts", "start": "node dist/index.js", "db:generate": "drizzle-kit generate", @@ -27,7 +28,7 @@ "test:scheduler": "tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts", "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 && npm run test:system-database", + "test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:alerts-telegram && 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 && npm run test:system-database", "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", diff --git a/backend/src/routes/alerts.ts b/backend/src/routes/alerts.ts index f11aefd6..4874483e 100644 --- a/backend/src/routes/alerts.ts +++ b/backend/src/routes/alerts.ts @@ -72,10 +72,11 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => { }) app.put("/alerts/telegram", async (req, reply) => { - const parsed = putTelegramSchema.safeParse(req.body) + const parsed = putTelegramSchema.strict().safeParse(req.body) if (!parsed.success) { return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) } + if (Object.keys(parsed.data).length === 0) return reply.status(400).send({error:"Нет изменённых параметров Telegram"}) const next = await updateTelegramSettings({ token: parsed.data.token, chatId: parsed.data.chatId, diff --git a/backend/src/services/alerts-service.ts b/backend/src/services/alerts-service.ts index e147e8b5..2c246f96 100644 --- a/backend/src/services/alerts-service.ts +++ b/backend/src/services/alerts-service.ts @@ -1,3 +1,5 @@ +import {sendTelegramRequest} from "./alerts-telegram-send.js" +import {executeTelegramSettingsUpdate} from "./alerts-telegram-settings.js" import { asc, desc, eq, sql } from "drizzle-orm" import { db, dbAll } from "../db/index.js" import { @@ -58,7 +60,7 @@ export type ApiAlertHistoryEntry = { async function ensureTelegramRow() { let row = (await db.select().from(alertTelegramSettings).where(eq(alertTelegramSettings.id, 1)).limit(1))[0] if (!row) { - await db.insert(alertTelegramSettings).values({ id: 1 }) + await db.insert(alertTelegramSettings).values({ id: 1 }).onConflictDoNothing({target:alertTelegramSettings.id}) row = (await db.select().from(alertTelegramSettings).where(eq(alertTelegramSettings.id, 1)).limit(1))[0] } return row! @@ -309,37 +311,13 @@ export async function updateTelegramSettings(patch: { /** undefined — не менять; null — сбросить тему */ messageThreadId?: number | null }) { - const cur = await ensureTelegramRow() - let nextToken = cur.botToken - let nextChat = cur.chatId ?? "" - let nextThread: number | null | undefined = undefined - - if (patch.token !== undefined) { - if (patch.token === null || patch.token === "") nextToken = "" - else nextToken = patch.token.trim() - } - if (patch.chatId !== undefined) nextChat = patch.chatId.trim() - if (patch.messageThreadId !== undefined) { - if (patch.messageThreadId === null) nextThread = null - else { - const n = Math.floor(patch.messageThreadId) - nextThread = Number.isFinite(n) && n >= 1 ? n : null - } - } - - const base = { - botToken: nextToken, - chatId: nextChat, - updatedAt: sql`now()`, - } - if (nextThread !== undefined) { - await db.update(alertTelegramSettings) - .set({ ...base, messageThreadId: nextThread }) - .where(eq(alertTelegramSettings.id, 1)) - } else { - await db.update(alertTelegramSettings).set(base).where(eq(alertTelegramSettings.id, 1)) - } - return await getTelegramPublic() + return executeTelegramSettingsUpdate(patch, { + ensure: ensureTelegramRow, + update: async values => { + const [row] = await db.update(alertTelegramSettings).set({...values, updatedAt:sql`now()`}).where(eq(alertTelegramSettings.id,1)).returning() + return row + }, + }) } /** Токен только для внутреннего вызова Telegram API (не отдаётся клиенту). */ @@ -395,26 +373,12 @@ export async function sendTelegramAlertMessage(opts: { ? Math.floor(opts.messageThreadId) : undefined : await getTelegramMessageThreadIdForApi() - const url = `https://api.telegram.org/bot${encodeURIComponent(token)}/sendMessage` - try { - const payload: { chat_id: string; text: string; message_thread_id?: number } = { - chat_id: chatId, - text: opts.text, - } - if (threadId != null) payload.message_thread_id = threadId - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - const data = (await res.json().catch(() => ({}))) as { ok?: boolean; description?: string } - if (!res.ok || data.ok === false) { - return { ok: false, error: data.description ?? res.statusText ?? "Telegram API error" } - } - return { ok: true } - } catch (e) { - return { ok: false, error: e instanceof Error ? e.message : "Ошибка сети" } + const payload: { chat_id: string; text: string; message_thread_id?: number } = { + chat_id: chatId, + text: opts.text, } + if (threadId != null) payload.message_thread_id = threadId + return sendTelegramRequest(token,payload) } export async function appendAlertHistory(entry: { diff --git a/backend/src/services/alerts-telegram-send.test.ts b/backend/src/services/alerts-telegram-send.test.ts new file mode 100644 index 00000000..88f0734a --- /dev/null +++ b/backend/src/services/alerts-telegram-send.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict" +import {test} from "node:test" +import {sendTelegramRequest} from "./alerts-telegram-send.js" +test("Telegram send requires HTTP success and literal true receipt; malformed body is never success",async()=>{ + for(const body of [{},{ok:false},{ok:"true"},null]){let calls=0;const result=await sendTelegramRequest("fixture-secret",{chat_id:"123",text:"fixture"},async()=>{calls++;return Response.json(body)});assert.equal(result.ok,false);assert.equal(calls,1)} + assert.equal((await sendTelegramRequest("fixture-secret",{chat_id:"123",text:"fixture"},async()=>new Response("html"))).ok,false) + assert.equal((await sendTelegramRequest("fixture-secret",{chat_id:"123",text:"fixture"},async()=>Response.json({ok:true},{status:503}))).ok,false) +}) +test("Telegram send uses bounded single POST and preserves confirmed result",async()=>{ + let calls=0;const result=await sendTelegramRequest("fixture:secret",{chat_id:"123",text:"fixture",message_thread_id:1},async(url,init)=>{calls++;assert.equal(url,"https://api.telegram.org/botfixture%3Asecret/sendMessage");assert.equal(init?.method,"POST");assert.ok(init?.signal);assert.deepEqual(JSON.parse(init?.body as string),{chat_id:"123",text:"fixture",message_thread_id:1});return Response.json({ok:true})});assert.deepEqual(result,{ok:true});assert.equal(calls,1) +}) +test("Telegram network unknown has no retry or secret; upstream error is redacted",async()=>{ + let calls=0;assert.deepEqual(await sendTelegramRequest("fixture:secret",{chat_id:"123",text:"fixture"},async()=>{calls++;throw Error("fixture:secret network URL")}),{ok:false,error:"Результат отправки в Telegram неизвестен: ответ не получен"});assert.equal(calls,1) + const result=await sendTelegramRequest("fixture:secret",{chat_id:"123",text:"fixture"},async()=>Response.json({ok:false,description:"fixture:secret fixture%3Asecret"}));assert.equal(result.ok,false);if(!result.ok)assert.equal(result.error,"[TOKEN] [TOKEN]") +}) diff --git a/backend/src/services/alerts-telegram-send.ts b/backend/src/services/alerts-telegram-send.ts new file mode 100644 index 00000000..e49fb55e --- /dev/null +++ b/backend/src/services/alerts-telegram-send.ts @@ -0,0 +1,11 @@ +type Payload={chat_id:string;text:string;message_thread_id?:number} +export async function sendTelegramRequest(token:string,payload:Payload,fetcher:typeof fetch=fetch):Promise<{ok:true}|{ok:false;error:string}>{ + const redact=(message:string)=>message.replaceAll(token,"[TOKEN]").replaceAll(encodeURIComponent(token),"[TOKEN]") + try{ + const response=await fetcher("https://api.telegram.org/bot"+encodeURIComponent(token)+"/sendMessage",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload),signal:AbortSignal.timeout(30_000)}) + const raw:unknown=await response.json().catch(()=>undefined) + if(response.ok&&typeof raw==="object"&&raw!==null&&"ok" in raw&&raw.ok===true)return {ok:true} + const description=typeof raw==="object"&&raw!==null&&"description" in raw&&typeof raw.description==="string"?raw.description:"Telegram не подтвердил приём сообщения" + return {ok:false,error:redact(description)} + }catch{return {ok:false,error:"Результат отправки в Telegram неизвестен: ответ не получен"}} +} diff --git a/backend/src/services/alerts-telegram-settings.test.ts b/backend/src/services/alerts-telegram-settings.test.ts new file mode 100644 index 00000000..c08d99d4 --- /dev/null +++ b/backend/src/services/alerts-telegram-settings.test.ts @@ -0,0 +1,9 @@ +import assert from "node:assert/strict" +import {test} from "node:test" +import {executeTelegramSettingsUpdate,type TelegramStoredPatch} from "./alerts-telegram-settings.js" +function fixture(){let row={botToken:"fixture-secret",chatId:"old",messageThreadId:10 as number|null};const writes:TelegramStoredPatch[]=[];return {writes,get:()=>row,set:(next:typeof row)=>row=next,deps:{ensure:async()=>{},update:async(patch:TelegramStoredPatch)=>{writes.push(patch);row={...row,...patch};return {...row}}}} +} +test("Telegram selective update changes only submitted fields and does not expose secret",async()=>{const f=fixture();const publicFields=await executeTelegramSettingsUpdate({chatId:" new "},f.deps);assert.deepEqual(f.writes,[{chatId:"new"}]);assert.deepEqual(f.get(),{botToken:"fixture-secret",chatId:"new",messageThreadId:10});assert.deepEqual(publicFields,{chatId:"new",tokenConfigured:true,messageThreadId:10});assert.equal(JSON.stringify(publicFields).includes("fixture-secret"),false)}) +test("Telegram disjoint concurrent patches preserve token/chat/thread without stale full-row writes",async()=>{const f=fixture();await Promise.all([executeTelegramSettingsUpdate({token:null},f.deps),executeTelegramSettingsUpdate({messageThreadId:null},f.deps),executeTelegramSettingsUpdate({chatId:"new"},f.deps)]);assert.deepEqual(f.get(),{botToken:"",chatId:"new",messageThreadId:null});assert.equal(f.writes.length,3);assert.ok(f.writes.every(row=>Object.keys(row).length===1))}) +test("Telegram invalid/empty patch does not ensure or update",async()=>{let calls=0;const deps={ensure:async()=>{calls++},update:async()=>{calls++;return undefined}};for(const patch of [{},{messageThreadId:0},{messageThreadId:1.5},{messageThreadId:2147483648},{token:4}])await assert.rejects(executeTelegramSettingsUpdate(patch as never,deps),/Проверьте/);assert.equal(calls,0)}) +test("Telegram missing/mismatched returning row does not confirm save or retry",async()=>{for(const row of [undefined,{botToken:"secret",chatId:"old",messageThreadId:null}]){let calls=0;await assert.rejects(executeTelegramSettingsUpdate({chatId:"new"},{ensure:async()=>{},update:async()=>{calls++;return row}}),/не подтверждено/);assert.equal(calls,1)}}) diff --git a/backend/src/services/alerts-telegram-settings.ts b/backend/src/services/alerts-telegram-settings.ts new file mode 100644 index 00000000..0487c4b7 --- /dev/null +++ b/backend/src/services/alerts-telegram-settings.ts @@ -0,0 +1,20 @@ +import {putTelegramSchema,telegramPublicSchema,type PutTelegramBody} from "@mmapp/contracts/alerts" +export type TelegramStoredRow={botToken:string;chatId:string;messageThreadId:number|null} +export type TelegramStoredPatch=Partial +interface Dependencies {ensure:()=>Promise;update:(patch:TelegramStoredPatch)=>Promise} +export async function executeTelegramSettingsUpdate(input:PutTelegramBody,deps:Dependencies){ + const parsed=putTelegramSchema.strict().safeParse(input) + if(!parsed.success||Object.keys(parsed.data).length===0)throw Error("Проверьте изменённые параметры Telegram") + const patch:TelegramStoredPatch={} + if(parsed.data.token!==undefined)patch.botToken=parsed.data.token?.trim()??"" + if(parsed.data.chatId!==undefined)patch.chatId=parsed.data.chatId.trim() + if(parsed.data.messageThreadId!==undefined)patch.messageThreadId=parsed.data.messageThreadId + if(Object.keys(patch).length===0)throw Error("Нет изменённых параметров Telegram") + await deps.ensure() + const row=await deps.update(patch) + if(!row||Object.entries(patch).some(([key,value])=>row[key as keyof TelegramStoredRow]!==value))throw Error("Сохранение параметров Telegram не подтверждено") + const result=telegramPublicSchema.safeParse({chatId:row.chatId,tokenConfigured:Boolean(row.botToken.trim()),messageThreadId:row.messageThreadId,connected:Boolean(row.botToken.trim()&&row.chatId.trim())}) + if(!result.success)throw Error("Сохранённые параметры Telegram не подтверждены") + const {connected:_connected,...publicFields}=result.data + return publicFields +} diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index d8a5e554..60d74183 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-03 -Этап 11Б в работе; последний проверенный блок 11Б.4.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.2.1: backend Telegram selective atomic save/RETURNING, DI HTTP/JWT/send receipts; затем rules concurrency и Alerts UI. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12. +Этап 11Б в работе; последний проверенный блок 11Б.4.2.1.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.2.1.2: DI Alerts HTTP/JWT read/write/save/test и согласованный источник Telegram; затем rules concurrency и Alerts UI. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). @@ -306,3 +306,5 @@ REST запрос → ожидание → подтверждённый output / 2026-10-03: 11Б.3.6.2.3.2 — 6 source и 18 соседних E2E прошли; база принята в локальных границах. Далее 11Б.4.1 Alerts contracts/API. 2026-10-03: 11Б.4.1 — строгие Alerts output schemas/API и сверка save receipts; 5 unit/builds/typecheck/lint прошли. Module пока не подключён. Далее 11Б.4.2.1 backend Telegram. + +2026-10-03: 11Б.4.2.1.1 — selective Telegram UPDATE RETURNING, strict send receipt; 7 DI/send tests/build/lint прошли. confirm-stability пропущен без PG. Далее HTTP/JWT 11Б.4.2.1.2. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index f67261b3..f9989931 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.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.2.1.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,13 @@ +## Блок 11Б.4.2.1.1 — selective Telegram save и подтверждение отправки (2026-10-03) + +- updateTelegramSettings больше не перезаписывает token/chat из предварительно прочитанного snapshot: строит только переданные columns, SQL UPDATE RETURNING подтверждает запись и сохраняет остальные поля. Singleton insert conflict-safe. DI helper проверяет patch/returning, public response не возвращает secret. Route отклоняет unknown/empty patch до update; thread ID ограничен положительным PostgreSQL int4 вместо silent floor/overflow. +- Send helper требует HTTP success и literal ok:true, HTML/{}/false/строка true не подтверждают приём Telegram. Один POST, 30s signal, без повтора внутри helper. При отсутствии ответа результат неизвестен; raw network error не раскрывается, upstream description редактирует literal/URL-encoded token. Подтверждение приёма API не является доказательством доставки получателю; существующая outbox retry policy не изменялась и ещё требует отдельного аудита unknown результатов. +- 4 DI save + 3 send-response tests, backend build/профильный lint прошли; 5 frontend API unit из предыдущего блока сохраняют совместимость. Alert-engine groups/evaluate tests прошли, confirm-stability пропущен из-за отсутствия PostgreSQL — не считать проверенным. Реальные DB concurrency/Telegram не запускались. +- Следующий блок 11Б.4.2.1.2: DI реальных Alerts HTTP handlers + signed JWT read/write gates и save/test receipts, согласованный источник Telegram для test. Затем rules transaction/readback concurrency и UI known/drafts/source/rights/adaptive. UI пока использует прежний generic API. + ## Блок 11Б.4.1 — строгие Alerts contracts/API (2026-10-03) - Аудит существующего UI: GET/PUT/POST принимают unchecked JSON; load заменяет Telegram draft и маскирует вторичные каталоги пустыми, save error вызывает load; целевая metadata иногда подменяется demo значениями. UI/backend пока не мигрированы этим блоком и не приняты. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 0bb78258..01684e0a 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -236,3 +236,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S Завершён 11Б.3.6.2.3.2: 6 source/ABA/reload и 18 General/DB E2E прошли, DB принят локально. Реальный PG/UDP worker/Portal не принят. Далее 11Б.4.1 — strict Alerts contracts/API, затем backend/UI. Завершён 11Б.4.1: строгие Alerts output/API/receipt сверки, 5 unit/contracts/backend build/typecheck/lint прошли. UI/backend пока прежние. Далее 11Б.4.2.1 — atomic selective Telegram save/RETURNING и DI HTTP/JWT/test results; затем rules/UI. + +Завершён 11Б.4.2.1.1: selective Telegram UPDATE RETURNING/partial concurrent patches, singleton conflict-safe и strict bounded send receipt; 7 tests/build/lint прошли. Groups/evaluate regression прошла, confirm-stability skip без PG. Далее 11Б.4.2.1.2 — Alerts real HTTP/JWT/save/test и согласованный источник; UI прежний. diff --git a/packages/contracts/src/alerts.ts b/packages/contracts/src/alerts.ts index 85b45133..b1b54120 100644 --- a/packages/contracts/src/alerts.ts +++ b/packages/contracts/src/alerts.ts @@ -58,7 +58,7 @@ export const putRulesSchema = z.object({ export const putTelegramSchema = z.object({ token: z.union([z.string(), z.null()]).optional(), chatId: z.string().optional(), - messageThreadId: z.union([z.number().int().positive(), z.null()]).optional(), + messageThreadId: z.union([z.number().int().positive().max(2147483647), z.null()]).optional(), }) export const rulePreviewTelegramSchema = z.object({ @@ -72,7 +72,7 @@ export const rulePreviewTelegramSchema = z.object({ export const testTelegramSchema = z.object({ token: z.string().optional(), chatId: z.string().optional(), - messageThreadId: z.coerce.number().int().positive().optional(), + messageThreadId: z.coerce.number().int().positive().max(2147483647).optional(), rulePreview: rulePreviewTelegramSchema.optional(), }) @@ -86,7 +86,7 @@ export type TestTelegramBody = z.infer // Public response schemas: configured credentials do not prove Telegram connectivity/delivery. const alertTimestampSchema = z.string().min(1).refine(value => Number.isFinite(Date.parse(value)), "Некорректное время оповещения") export const telegramPublicSchema = z.object({ - chatId:z.string(),tokenConfigured:z.boolean(),connected:z.boolean(),messageThreadId:z.number().int().positive().nullable(), + chatId:z.string(),tokenConfigured:z.boolean(),connected:z.boolean(),messageThreadId:z.number().int().positive().max(2147483647).nullable(), }).strict().refine(value => value.connected === (value.tokenConfigured && Boolean(value.chatId.trim())), "Противоречивое состояние параметров Telegram") export const alertRuleResponseSchema = z.object({ id:z.string().min(1),name:z.string().min(1),type:alertTypeSchema,target:z.string(),targets:z.array(z.string().min(1)),groupId:z.string().nullable(),condition:z.string(),conditions:z.array(z.string().min(1)),severity:alertSeveritySchema,enabled:z.boolean(),cooldown:alertCooldownSchema,