From a0263c22a53bbb4a38900d52dd8b19847d1d3add Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 21:59:40 +0700 Subject: [PATCH] =?UTF-8?q?fix(collectors):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=8F=D1=82=D1=8C=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 11 ++++++- docs/ux-ui-stage-11b.md | 10 ++++++ shared/api/collector-settings.test.ts | 44 +++++++++++++++++++++++++++ shared/api/collector-settings.ts | 26 ++++++++++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 shared/api/collector-settings.test.ts create mode 100644 shared/api/collector-settings.ts diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 89b6e0e1..7b5b71ac 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -226,7 +226,7 @@ REST запрос → ожидание → подтверждённый output / | Пользователи | [Профиль](ux-ui/screens/users-profile-desktop.png), [ошибка](ux-ui/screens/users-error-desktop.png) | [390](ux-ui/screens/users-profile-mobile.png), [320](ux-ui/screens/users-profile-narrow.png) | | Контейнеры | [Обзор](ux-ui/screens/containers-overview-desktop.png), [снимок](ux-ui/screens/containers-export-desktop.png) | [390](ux-ui/screens/containers-export-mobile.png), [320](ux-ui/screens/containers-export-narrow.png) | -Актуальная точка продолжения: 11Б.3.3.4 — strict collector settings API и различия DTO. Независимые scheduler status/runs и достоверные KPI приняты в 11Б.3.3.3. Strict history/unknown/свежесть приняты в 11Б.3.3.2. Контекст/abort/revision и исходный lint исправлены в 11Б.3.3.1; далее независимые readers/dirty states и операции. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал. +Актуальная точка продолжения: 11Б.3.3.5 — подключение strict settings readers, независимые known/error/time и dirty drafts. API основа проверена в 11Б.3.3.4; пока не подключена к странице. Независимые scheduler status/runs и достоверные KPI приняты в 11Б.3.3.3. Strict history/unknown/свежесть приняты в 11Б.3.3.2. Контекст/abort/revision и исходный lint исправлены в 11Б.3.3.1; далее независимые readers/dirty states и операции. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал. | Экран | ПК | Телефон | | --- | --- | --- | diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 2c441d2f..6a83e35e 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.3.3; независимые status/history и достоверные KPI проверены; далее settings/dirty формы | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.4; status/history/KPI и strict settings API основа проверены; далее подключение readers/dirty формы | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,15 @@ +## Блок 11Б.3.3.4 — строгие readers настроек сборщиков (2026-10-01) + +- Добавлен shared/api/collector-settings.ts: traffic/REST ping/uptime/internet path читаются по своим реальным GET DTO. False/zero/null сохраняются, missing/invalid не нормализуется defaults; PostgreSQL время приводится к тому же ISO instant. +- REST ping не получает выдуманный retentionDays. Traffic/internet path требуют фактический retention; uptime требует отдельные resources/ping/speed и интервалы. Вложенная scheduler metadata uptime не используется: status уже получает отдельный строгий reader. Сертификаты имеют существующий strict getCertificateRenewSettings, его следует переиспользовать. +- 10 новых API unit, полный TypeScript и ESLint новых файлов прошли. Production frontend прошёл в 11Б.3.3.3; этот блок добавляет пока не подключённый модуль, UI/backend не менялись. +- Current UI всё ещё использует старые settings calls. Нельзя считать формы подтверждёнными или защищёнными от default submit по этим unit: подключение readers/known/error/time/dirty states, synchronous locks, RBAC и unknown mutation receipt ещё впереди. Generic CollectorSettingsDto на странице ошибочно требует retention у REST ping; при подключении исправить типы по отдельным DTO. +- **Следующий блок 11Б.3.3.5:** подключить четыре strict settings reader и существующий certificate reader, независимые состояния/свежесть и dirty drafts; затем операции/права/source E2E. Evolution malformed snapshot crash и GeoIP/NetFlow формы остаются в scope 11Б. +- На контрольной точке недельный лимит использован на 99%, пятичасовой на 36%. Проверенные блоки сохранены; для продолжения читать текущую квоту и этот журнал. + ## Блок 11Б.3.3.3 — независимые scheduler status/runs и достоверные KPI (2026-10-01) - Strict getSchedulerStatus подключён как отдельный источник. Status/runs публикуются сразу по своим ответам, до завершения пяти settings зависимостей; медленный/ошибочный uptime не задерживает подтверждённую сводку и историю. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 9a999d51..87db811c 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -91,3 +91,13 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S - Первый status fixture по ошибке включал все интеграции generic enabled=true, выявив старый malformed Evolution snapshot crash в sidebar. Fixture ограничена выключенной интеграцией; этот отдельный дефект Evolution reader включён в оставшийся scope 11Б, исправление не заявлено. - Остальные settings всё ещё обрабатываются общим Promise.allSettled и могут сбрасывать drafts; strict settings readers/dirty state и операции впереди. GeoIP/NetFlow/общий UI ещё не принят, реальные DB/RouterOS/Portal остаются внешней приёмкой. - **Следующий блок 11Б.3.3.4:** strict collector settings API с реальными различиями DTO; затем подключение независимых settings readers и dirty drafts, mutation/RBAC/source проверки. + + +## Блок 11Б.3.3.4 — строгие readers настроек сборщиков (2026-10-01) + +- Добавлен shared/api/collector-settings.ts: traffic/REST ping/uptime/internet path читаются по своим реальным GET DTO. False/zero/null сохраняются, missing/invalid не нормализуется defaults; PostgreSQL время приводится к тому же ISO instant. +- REST ping не получает выдуманный retentionDays. Traffic/internet path требуют фактический retention; uptime требует отдельные resources/ping/speed и интервалы. Вложенная scheduler metadata uptime не используется: status уже получает отдельный строгий reader. Сертификаты имеют существующий strict getCertificateRenewSettings, его следует переиспользовать. +- 10 новых API unit, полный TypeScript и ESLint новых файлов прошли. Production frontend прошёл в 11Б.3.3.3; этот блок добавляет пока не подключённый модуль, UI/backend не менялись. +- Current UI всё ещё использует старые settings calls. Нельзя считать формы подтверждёнными или защищёнными от default submit по этим unit: подключение readers/known/error/time/dirty states, synchronous locks, RBAC и unknown mutation receipt ещё впереди. Generic CollectorSettingsDto на странице ошибочно требует retention у REST ping; при подключении исправить типы по отдельным DTO. +- **Следующий блок 11Б.3.3.5:** подключить четыре strict settings reader и существующий certificate reader, независимые состояния/свежесть и dirty drafts; затем операции/права/source E2E. Evolution malformed snapshot crash и GeoIP/NetFlow формы остаются в scope 11Б. +- На контрольной точке недельный лимит использован на 99%, пятичасовой на 36%. Проверенные блоки сохранены; для продолжения читать текущую квоту и этот журнал. diff --git a/shared/api/collector-settings.test.ts b/shared/api/collector-settings.test.ts new file mode 100644 index 00000000..3e46e2e6 --- /dev/null +++ b/shared/api/collector-settings.test.ts @@ -0,0 +1,44 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings } from "./collector-settings" +const base = "https://api.example.test" +const common = { enabled: false, intervalSec: 60, lastCollectedAt: null, lastDurationMs: 0, lastError: null, collectorRunning: false } +const retained = { ...common, retentionDays: 14 } +const uptime = { ...retained, resourcesEnabled: false, pingEnabled: true, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 } +const json = (body: unknown) => new Response(JSON.stringify(body)) +for (const [name, read, payload, path] of [ + ["traffic", getTrafficCollectorSettings, retained, "/api/traffic/settings"], + ["REST ping", getServersApiPingSettings, common, "/api/servers-api-ping/settings"], + ["uptime", getUptimeCollectorSettings, uptime, "/api/uptime/settings"], + ["internet path", getInternetPathSettings, retained, "/api/internet-path/settings"], +] as const) { + test(name + ": exact GET preserves false/zero/null and PostgreSQL timestamp instant", async () => { + const original = globalThis.fetch + try { + globalThis.fetch = async (url, init) => { assert.equal(new URL(String(url)).pathname, path); assert.equal(init?.method, "GET"); return json(payload) } + const result = await read(base); assert.equal(result.enabled, false); assert.equal(result.lastDurationMs, 0); assert.equal(result.lastCollectedAt, null) + globalThis.fetch = async () => json({ ...payload, lastCollectedAt: "2026-10-01 00:00:00+00" }); assert.equal((await read(base)).lastCollectedAt, "2026-10-01T00:00:00.000Z") + } finally { globalThis.fetch = original } + }) + test(name + ": malformed response is unknown without field defaults", async () => { + const original = globalThis.fetch + try { + for (const raw of [{}, { ...payload, enabled: undefined }, { ...payload, intervalSec: "60" }, { ...payload, intervalSec: 0 }, { ...payload, lastDurationMs: -1 }, { ...payload, lastCollectedAt: "bad" }]) { globalThis.fetch = async () => json(raw); await assert.rejects(() => read(base), /Настройки сборщика не подтверждены/) } + } finally { globalThis.fetch = original } + }) +} +test("REST ping does not invent retention, uptime preserves separate flags and ignores nested scheduler metadata", async () => { + const original = globalThis.fetch + try { + globalThis.fetch = async () => json(common); assert.equal("retentionDays" in await getServersApiPingSettings(base), false) + globalThis.fetch = async () => json({ ...uptime, scheduler: { jobs: "malformed independent metadata" } }); const result = await getUptimeCollectorSettings(base); assert.equal(result.resourcesEnabled, false); assert.equal(result.pingEnabled, true); assert.equal(result.speedEnabled, false); assert.equal("scheduler" in result, false) + for (const key of ["resourcesEnabled", "pingEnabled", "speedEnabled", "probeIntervalSec", "speedIntervalSec"]) { globalThis.fetch = async () => json({ ...uptime, [key]: undefined }); await assert.rejects(() => getUptimeCollectorSettings(base)) } + } finally { globalThis.fetch = original } +}) +test("retained collectors require actual retention and caller cancellation is preserved", async () => { + const original = globalThis.fetch + try { + globalThis.fetch = async () => json(common); await assert.rejects(() => getTrafficCollectorSettings(base)); await assert.rejects(() => getInternetPathSettings(base)) + const controller = new AbortController(); controller.abort(); globalThis.fetch = async (_url, init) => { assert.equal(init?.signal?.aborted, true); throw controller.signal.reason }; await assert.rejects(() => getTrafficCollectorSettings(base, { signal: controller.signal })) + } finally { globalThis.fetch = original } +}) diff --git a/shared/api/collector-settings.ts b/shared/api/collector-settings.ts new file mode 100644 index 00000000..ede84484 --- /dev/null +++ b/shared/api/collector-settings.ts @@ -0,0 +1,26 @@ +import { z } from "zod" +import { requestJson } from "./http-client" +const count = z.number().int().nonnegative().safe() +const timestamp = z.string().min(1).refine(value => Number.isFinite(Date.parse(value))).transform(value => new Date(value).toISOString()) +const common = z.object({ enabled: z.boolean(), intervalSec: count.positive(), lastCollectedAt: timestamp.nullable(), lastDurationMs: count.nullable(), lastError: z.string().nullable(), collectorRunning: z.boolean().optional() }) +const retained = common.extend({ retentionDays: count.positive() }) +const uptime = retained.extend({ resourcesEnabled: z.boolean(), pingEnabled: z.boolean(), speedEnabled: z.boolean(), probeIntervalSec: count.positive(), speedIntervalSec: count.positive() }) +async function settings(baseUrl: string, path: string, schema: z.ZodType, init?: RequestInit): Promise { + const raw = await requestJson(baseUrl, path, { ...init, method: "GET", signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) }) + const result = schema.safeParse(raw) + if (!result.success) throw new Error("Настройки сборщика не подтверждены: получен некорректный ответ сервера") + return result.data +} +export function getTrafficCollectorSettings(baseUrl: string, init?: RequestInit) { + return settings(baseUrl, "/api/traffic/settings", retained, init) +} +export function getServersApiPingSettings(baseUrl: string, init?: RequestInit) { + return settings(baseUrl, "/api/servers-api-ping/settings", common, init) +} +export function getUptimeCollectorSettings(baseUrl: string, init?: RequestInit) { + // Scheduler metadata is obtained independently through the scheduler API. + return settings(baseUrl, "/api/uptime/settings", uptime, init) +} +export function getInternetPathSettings(baseUrl: string, init?: RequestInit) { + return settings(baseUrl, "/api/internet-path/settings", retained, init) +}