fix(evobgp): проверять каталог и сохранять навигацию при ошибках API
This commit is contained in:
1 parent
a0263c22a5
commit
0d2affaf6b
7 files changed
+77
-4
No files matched your search
+1
-1
@@ -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.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; внешние ограничения — журнал.
|
||||
Каталог Evolution защищён от неполного HTTP-ответа в 11Б.3.3.4.1; testConnection receipt и состояния интеграции остаются в объёме 11Б. Актуальная точка продолжения: 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; внешние ограничения — журнал.
|
||||
|
||||
| Экран | ПК | Телефон |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -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.4; status/history/KPI и strict settings API основа проверены; далее подключение readers/dirty формы |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.4.1; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## Блок 11Б.3.3.4.1 — проверка каталога Evolution (2026-10-01)
|
||||
|
||||
- Каталог Evolution проверяется до записи в состояние: обязательные коллекции, поля строк, дата снимка, числовые счётчики и уникальность идентификаторов. Неполный HTTP 200 больше не обрушает боковую навигацию; в настройках EvoBGP отображается понятная ошибка.
|
||||
- Подтверждённые пустые коллекции, false и zero сохраняются. Decoder совместим с DTO backend; неизвестные значения backend не переосмыслены. Последний снимок при ошибке пока не сохраняется: независимые состояния, свежесть и защита от гонок Evolution остаются в объёме этапа.
|
||||
- Прошли 3 unit, 1 браузерный тест через обычные API-пути с подменой HTTP, ESLint изменённых файлов и production frontend/contracts build со встроенным TypeScript. В браузере проверены доступность журнала, переход в настройки EvoBGP и отсутствие pageerror при malformed каталоге. Реальный Evolution Portal не проверен.
|
||||
- Следующий основной блок остаётся 11Б.3.3.5: подключение strict settings readers, независимые known/error/time и dirty drafts. Также выявлено: Evolution testConnection пока принимает любой успешный HTTP вместо подтверждения ok=true; исправление и профильная проверка ещё впереди.
|
||||
- На завершении пятичасовой лимит использован на 42%, недельный показывает 100%. Новый блок не начат; изменения сохранены локально без push.
|
||||
|
||||
## Блок 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.
|
||||
|
||||
@@ -101,3 +101,12 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
|
||||
- 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.4.1 — проверка каталога Evolution (2026-10-01)
|
||||
|
||||
- Каталог Evolution проверяется до записи в состояние: обязательные коллекции, поля строк, дата снимка, числовые счётчики и уникальность идентификаторов. Неполный HTTP 200 больше не обрушает боковую навигацию; в настройках EvoBGP отображается понятная ошибка.
|
||||
- Подтверждённые пустые коллекции, false и zero сохраняются. Decoder совместим с DTO backend; неизвестные значения backend не переосмыслены. Последний снимок при ошибке пока не сохраняется: независимые состояния, свежесть и защита от гонок Evolution остаются в объёме этапа.
|
||||
- Прошли 3 unit, 1 браузерный тест через обычные API-пути с подменой HTTP, ESLint изменённых файлов и production frontend/contracts build со встроенным TypeScript. В браузере проверены доступность журнала, переход в настройки EvoBGP и отсутствие pageerror при malformed каталоге. Реальный Evolution Portal не проверен.
|
||||
- Следующий основной блок остаётся 11Б.3.3.5: подключение strict settings readers, независимые known/error/time и dirty drafts. Также выявлено: Evolution testConnection пока принимает любой успешный HTTP вместо подтверждения ok=true; исправление и профильная проверка ещё впереди.
|
||||
- На завершении пятичасовой лимит использован на 42%, недельный показывает 100%. Новый блок не начат; изменения сохранены локально без push.
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { test } from "node:test"
|
||||
import { parseEvoBgpCatalog } from "./evobgp-catalog"
|
||||
const empty = { fetchedAt: "2026-10-01T00:00:00Z", modules: [], domains: [], ipRanges: [], asns: [], communities: [] }
|
||||
const domain = { id: "domain-1", domain: "example.test", resolvedIp: "—", asn: "—", purpose: "", filter: "", updated: "—", enabled: false }
|
||||
test("known empty and false stay explicit without invented rows", () => {
|
||||
assert.deepEqual(parseEvoBgpCatalog(empty), empty)
|
||||
assert.equal(parseEvoBgpCatalog({ ...empty, domains: [domain] }).domains[0].enabled, false)
|
||||
})
|
||||
test("malformed or partial catalogue never reaches consumers as a snapshot", () => {
|
||||
for (const raw of [{}, null, { ...empty, domains: undefined }, { ...empty, asns: {} }, { ...empty, fetchedAt: "bad" }, { ...empty, domains: [null] }, { ...empty, domains: [{ ...domain, enabled: undefined }] }, { ...empty, domains: [domain, domain] }]) assert.throws(() => parseEvoBgpCatalog(raw), /Каталог Evolution не подтверждён/)
|
||||
})
|
||||
test("ASN and community zero counts are preserved, invalid counts rejected", () => {
|
||||
const asn = { id: "asn-1", asn: "AS1", org: "", country: "", prefixes: 0, filter: "", updated: "—", enabled: false }
|
||||
assert.equal(parseEvoBgpCatalog({ ...empty, asns: [asn] }).asns[0].prefixes, 0)
|
||||
assert.throws(() => parseEvoBgpCatalog({ ...empty, asns: [{ ...asn, prefixes: -1 }] }))
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod"
|
||||
import type { EvoBgpCatalogSnapshot } from "./evobgp-context"
|
||||
const text = z.string()
|
||||
const id = text.min(1)
|
||||
const count = z.number().int().nonnegative().safe()
|
||||
function unique<T extends z.ZodType<{ id: string }>>(row: T) {
|
||||
return z.array(row).refine(rows => new Set(rows.map(item => item.id)).size === rows.length)
|
||||
}
|
||||
const base = { id, asn: text, purpose: text, filter: text, updated: text, enabled: z.boolean() }
|
||||
const schema = z.object({
|
||||
fetchedAt: text.refine(value => Number.isFinite(Date.parse(value))),
|
||||
modules: unique(z.object({ id, name: text, type: text })),
|
||||
domains: unique(z.object({ ...base, domain: text, resolvedIp: text })),
|
||||
ipRanges: unique(z.object({ ...base, cidr: text, country: text })),
|
||||
asns: unique(z.object({ id, asn: text, org: text, country: text, prefixes: count, filter: text, updated: text, enabled: z.boolean() })),
|
||||
communities: unique(z.object({ id, value: text, name: text, description: text, type: z.enum(["standard", "no-export", "no-advertise", "local-as", "custom"]), filterIds: z.array(text), serverCount: count, prefixCount: count, action: z.enum(["permit", "deny", "local-pref", "metric"]), actionValue: z.number().finite().optional(), enabled: z.boolean() })),
|
||||
})
|
||||
export function parseEvoBgpCatalog(raw: unknown): EvoBgpCatalogSnapshot {
|
||||
const parsed = schema.safeParse(raw)
|
||||
if (!parsed.success) throw new Error("Каталог Evolution не подтверждён: получен некорректный ответ сервера")
|
||||
return parsed.data
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "react"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import type { Domain, IpRange, Asn } from "@/lib/data"
|
||||
import { parseEvoBgpCatalog } from "@/lib/evobgp-catalog"
|
||||
import { ApiClientError, requestJson } from "@/shared/api/http-client"
|
||||
|
||||
export interface EvoBgpCommunityRow {
|
||||
@@ -93,12 +94,12 @@ export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await requestJson<EvoBgpCatalogSnapshot>(
|
||||
const data = await requestJson<unknown>(
|
||||
backendUrl,
|
||||
"/api/evobgp/catalog",
|
||||
{ method: "POST" },
|
||||
)
|
||||
setSnapshot(data)
|
||||
setSnapshot(parseEvoBgpCatalog(data))
|
||||
} catch (e) {
|
||||
setSnapshot(null)
|
||||
setError(errorMessage(e, "Ошибка загрузки"))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
test("Evolution: malformed catalogue не обрушает навигацию", async ({ page }) => {
|
||||
const errors: string[] = []; let catalogs = 0; page.on("pageerror", error => errors.push(error.message))
|
||||
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
|
||||
await page.route("**/api/**", r => {
|
||||
const path = new URL(r.request().url()).pathname
|
||||
if (path === "/api/auth/config") return r.fulfill({ json: { required: false } })
|
||||
if (path === "/api/evobgp/settings") return r.fulfill({ json: { baseUrl: "https://evo.example.test", enabled: true, secretConfigured: true } })
|
||||
if (path === "/api/evobgp/catalog") { catalogs++; return r.fulfill({ json: {} }) }
|
||||
return r.fulfill({ status: 503, json: { error: "outside catalogue fixture" } })
|
||||
})
|
||||
await page.goto("/data-collection"); await expect.poll(() => catalogs).toBeGreaterThan(0)
|
||||
await expect(page.getByText("Журнал прогонов", { exact: true })).toBeVisible()
|
||||
await page.goto("/settings"); await page.getByRole("button", { name: "EvoBGP", exact: true }).click(); await expect(page.getByText("Каталог Evolution не подтверждён: получен некорректный ответ сервера", { exact: true })).toBeVisible()
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
Reference in new issue
Block a user