From 117aa9a21a8a01539ed12ea24113e764cf947555 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 3 Oct 2026 11:43:35 +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=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D1=8B=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= 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 | 8 +++++- docs/ux-ui-stage-11b.md | 6 ++++ .../api/collector-settings-mutations.test.ts | 24 ++++++++++++++++ shared/api/collector-settings.ts | 28 +++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 shared/api/collector-settings-mutations.test.ts diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 8248d251..42696ce1 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) | -Каталог Evolution защищён от неполного HTTP-ответа в 11Б.3.3.4.1; Подтверждение testConnection исправлено в 11Б.3.3.4.2; состояния интеграции остаются в объёме 11Б. Актуальная точка продолжения: 11Б.3.3.6 — точечные операции настроек, строгие receipts и ввод, locks/права и подтверждённый сброс dirty. Независимые strict settings readers и защита черновиков подключены в 11Б.3.3.5; реальные интеграции остаются внешней приёмкой. Независимые 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; внешние ограничения — журнал. +Каталог Evolution защищён от неполного HTTP-ответа в 11Б.3.3.4.1; Подтверждение testConnection исправлено в 11Б.3.3.4.2; состояния интеграции остаются в объёме 11Б. Актуальная точка продолжения: 11Б.3.3.6.2 — точечные операции настроек, строгие receipts и ввод, locks/права и подтверждённый сброс dirty. Независимые strict settings readers и защита черновиков подключены в 11Б.3.3.5; реальные интеграции остаются внешней приёмкой. Независимые 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 382d668f..3a54d60e 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.5; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.6.1; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,12 @@ +## Блок 11Б.3.3.6.1 — строгие PUT настроек (2026-10-03) + +- Четыре settings PUT проверяют минимумы/целые числа до запроса и реальные backend receipt ok=true/settings после него. Каждый переданный параметр обязан совпасть с подтверждённым. Пустой patch/unknown fields/неверные числа отклоняются; timeout/ошибка/неверный receipt не вызывают повтор. +- Прошли 14 GET/PUT unit, ESLint и полный TypeScript. Первоначальный unit assertion уточнён под две формулировки подтверждения; повтор прошёл. Новый API пока не подключён к операциям страницы; certificate PUT переиспользуется с отдельной проверкой echo на следующем шаге. +- Далее 11Б.3.3.6.2: точечная запись dirty полей и выбранного переключателя, locks/права, сброс dirty по подтверждению и профильные E2E. + ## Блок 11Б.3.3.5 — независимые настройки и черновики (2026-10-03) - Подключены четыре строгих settings reader и существующий certificate reader. У каждого источника независимые known/error/browser receivedAt; медленный uptime не задерживает остальные настройки. Ошибка сохраняет последние подтверждённые данные и время, неполный DTO не подменяется defaults. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 4485b4f5..e0e53891 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -125,3 +125,9 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S - Неизвестные настройки показывают —/пустые отключённые поля, доступность записи пока консервативно зависит от всех пяти подтверждённых источников. GeoIP расписание отмечено внешне управляемым через NetFlow. Формат/композиция установленной Data Grid сохранены; async metadata объявляется role=status. - Прошли 15 профильных E2E (4 новых settings + история/status/race), ESLint, полный TypeScript и production frontend/contracts build. ReUI audit checklist получен; изменения затрагивают данные/состояния, vendor props и тема не менялись. Реальные DB/RouterOS/ACME не проверены. - Следующий блок 11Б.3.3.6: строгие mutation receipts/проверка ввода, точечные PUT вместо записи всех настроек переключателем, синхронные locks/права и сброс dirty только по подтверждённому результату. Полная приёмка 11Б ещё впереди. + +## Блок 11Б.3.3.6.1 — строгие PUT настроек (2026-10-03) + +- Четыре settings PUT проверяют минимумы/целые числа до запроса и реальные backend receipt ok=true/settings после него. Каждый переданный параметр обязан совпасть с подтверждённым. Пустой patch/unknown fields/неверные числа отклоняются; timeout/ошибка/неверный receipt не вызывают повтор. +- Прошли 14 GET/PUT unit, ESLint и полный TypeScript. Первоначальный unit assertion уточнён под две формулировки подтверждения; повтор прошёл. Новый API пока не подключён к операциям страницы; certificate PUT переиспользуется с отдельной проверкой echo на следующем шаге. +- Далее 11Б.3.3.6.2: точечная запись dirty полей и выбранного переключателя, locks/права, сброс dirty по подтверждению и профильные E2E. diff --git a/shared/api/collector-settings-mutations.test.ts b/shared/api/collector-settings-mutations.test.ts new file mode 100644 index 00000000..68c16932 --- /dev/null +++ b/shared/api/collector-settings-mutations.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { putTrafficCollectorSettings, putServersApiPingSettings, putUptimeCollectorSettings, putInternetPathSettings } from "./collector-settings" +const common = { enabled: false, intervalSec: 300, retentionDays: 14, lastCollectedAt: null, lastDurationMs: 0, lastError: null } +const uptime = { ...common, resourcesEnabled: false, pingEnabled: false, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 } +for (const [name, put, path, dto] of [ + ["traffic", putTrafficCollectorSettings, "/api/traffic/settings", common], + ["REST ping", putServersApiPingSettings, "/api/servers-api-ping/settings", common], + ["uptime", putUptimeCollectorSettings, "/api/uptime/settings", uptime], + ["internet", putInternetPathSettings, "/api/internet-path/settings", common], +] as const) test(name + ": selective PUT requires exact explicit receipt without retries", async () => { + const original = globalThis.fetch; let calls = 0 + try { + globalThis.fetch = async (url, init) => { calls++; assert.equal(new URL(String(url)).pathname, path); assert.equal(init?.method, "PUT"); assert.deepEqual(JSON.parse(String(init.body)), { intervalSec: 300 }); return new Response(JSON.stringify({ ok: true, settings: dto })) } + assert.equal((await put("http://fixture.test", { intervalSec: 300 })).intervalSec, 300); assert.equal(calls, 1) + for (const raw of [{}, { ok: false, settings: dto }, { ok: true, settings: {} }, { ok: true, settings: { ...dto, intervalSec: 301 } }]) { + calls = 0; globalThis.fetch = async () => { calls++; return new Response(JSON.stringify(raw)) } + await assert.rejects(() => put("http://fixture.test", { intervalSec: 300 }), /подтвер/); assert.equal(calls, 1) + } + for (const payload of [{ intervalSec: 0 }, { intervalSec: 30.5 }, { intervalSec: NaN }, {}]) { + calls = 0; await assert.rejects(() => put("http://fixture.test", payload)); assert.equal(calls, 0) + } + } finally { globalThis.fetch = original } +}) diff --git a/shared/api/collector-settings.ts b/shared/api/collector-settings.ts index ede84484..a7c658c5 100644 --- a/shared/api/collector-settings.ts +++ b/shared/api/collector-settings.ts @@ -24,3 +24,31 @@ export function getUptimeCollectorSettings(baseUrl: string, init?: RequestInit) export function getInternetPathSettings(baseUrl: string, init?: RequestInit) { return settings(baseUrl, "/api/internet-path/settings", retained, init) } + +const trafficPatch = z.object({ enabled: z.boolean().optional(), intervalSec: count.min(5).optional(), retentionDays: count.positive().optional() }).strict() +const restPatch = z.object({ enabled: z.boolean().optional(), intervalSec: count.min(10).optional() }).strict() +const uptimePatch = z.object({ resourcesEnabled: z.boolean().optional(), pingEnabled: z.boolean().optional(), speedEnabled: z.boolean().optional(), intervalSec: count.min(5).optional(), probeIntervalSec: count.min(5).optional(), speedIntervalSec: count.min(10).optional(), retentionDays: count.positive().optional() }).strict() +const internetPatch = z.object({ enabled: z.boolean().optional(), intervalSec: count.min(30).optional(), retentionDays: count.positive().optional() }).strict() +async function putSettings>(baseUrl: string, path: string, payload: P, input: z.ZodType

, output: z.ZodType, init?: RequestInit): Promise { + const valid = input.safeParse(payload) + if (!valid.success || !Object.keys(valid.data).some(key => valid.data[key] !== undefined)) throw new Error("Укажите допустимые целые значения настроек сборщика") + const body = valid.data + const raw = await requestJson(baseUrl, path, { ...init, method: "PUT", body: JSON.stringify(body), signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) }) + const parsed = z.object({ ok: z.literal(true), settings: output }).safeParse(raw) + if (!parsed.success) throw new Error("Сохранение настроек не подтверждено. Обновите данные перед повторной попыткой") + const confirmed = parsed.data.settings as Record + if (Object.entries(body).some(([key, value]) => value !== undefined && confirmed[key] !== value)) throw new Error("Сервер подтвердил другие настройки. Обновите данные перед повторной попыткой") + return parsed.data.settings +} +export function putTrafficCollectorSettings(baseUrl: string, payload: z.infer, init?: RequestInit) { + return putSettings(baseUrl, "/api/traffic/settings", payload, trafficPatch, retained, init) +} +export function putServersApiPingSettings(baseUrl: string, payload: z.infer, init?: RequestInit) { + return putSettings(baseUrl, "/api/servers-api-ping/settings", payload, restPatch, common, init) +} +export function putUptimeCollectorSettings(baseUrl: string, payload: z.infer, init?: RequestInit) { + return putSettings(baseUrl, "/api/uptime/settings", payload, uptimePatch, uptime, init) +} +export function putInternetPathSettings(baseUrl: string, payload: z.infer, init?: RequestInit) { + return putSettings(baseUrl, "/api/internet-path/settings", payload, internetPatch, retained, init) +}