fix(api): закрепить неизвестное состояние BFD в строгом каталоге
This commit is contained in:
1 parent
b1080b0a05
commit
47dcd89b3c
4 files changed
+36
-5
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
import assert from "node:assert/strict"
|
import assert from "node:assert/strict"
|
||||||
import Fastify from "fastify"
|
import Fastify from "fastify"
|
||||||
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
||||||
import { createOspfRoutes } from "./ospf.js"
|
import { createOspfRoutes, parseBfdSessions } from "./ospf.js"
|
||||||
import { emptyOspfCatalog, pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
import { emptyOspfCatalog, pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
const devices = [{ id: 1, name: "Первый" }, { id: 2, name: "Второй" }]
|
const devices = [{ id: 1, name: "Первый" }, { id: 2, name: "Второй" }]
|
||||||
@@ -29,3 +29,11 @@ assert.equal(response.statusCode, 200)
|
|||||||
assert.deepEqual(response.json(), mixed)
|
assert.deepEqual(response.json(), mixed)
|
||||||
await app.close()
|
await app.close()
|
||||||
console.log("OSPF catalog: partial/empty/failed/foreign/duplicate and strict BFD/routes, HTTP passed")
|
console.log("OSPF catalog: partial/empty/failed/foreign/duplicate and strict BFD/routes, HTTP passed")
|
||||||
|
|
||||||
|
const bfdServer = { id: 1, name: "Первый", host: "192.0.2.1", site: "", country: "RU" } as typeof servers.$inferSelect
|
||||||
|
const rawBfd = (state?: string) => [{ ".id": "*1", "local-address": "192.0.2.1%ether1", "remote-address": "192.0.2.2", ...(state === undefined ? {} : { state }) }]
|
||||||
|
for (const state of [undefined, "", " "]) assert.equal(parseBfdSessions(bfdServer, rawBfd(state), true)[0].state, "Unknown")
|
||||||
|
assert.equal(parseBfdSessions(bfdServer, rawBfd("down"), true)[0].state, "Down")
|
||||||
|
assert.equal(parseBfdSessions(bfdServer, rawBfd("up"), true)[0].state, "Up")
|
||||||
|
assert.equal(parseBfdSessions(bfdServer, rawBfd(), false)[0].state, "Down")
|
||||||
|
console.log("OSPF strict BFD missing/blank/known states and legacy compatibility passed")
|
||||||
@@ -89,7 +89,7 @@ async function fetchServerOspf(server: ServerRow) {
|
|||||||
|
|
||||||
// ── BFD parser ────────────────────────────────────────────────────────────────
|
// ── BFD parser ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function parseBfdSessions(server: ServerRow, raw: RosBfdSession[]): BfdSessionRead[] {
|
export function parseBfdSessions(server: ServerRow, raw: RosBfdSession[], strictState = false): BfdSessionRead[] {
|
||||||
return raw.map((s, idx) => {
|
return raw.map((s, idx) => {
|
||||||
const local = parseAddrIface(s["local-address"])
|
const local = parseAddrIface(s["local-address"])
|
||||||
const remote = parseAddrIface(s["remote-address"])
|
const remote = parseAddrIface(s["remote-address"])
|
||||||
@@ -105,7 +105,7 @@ function parseBfdSessions(server: ServerRow, raw: RosBfdSession[]): BfdSessionRe
|
|||||||
localAddr: local.ip,
|
localAddr: local.ip,
|
||||||
remoteAddr: remote.ip,
|
remoteAddr: remote.ip,
|
||||||
interface: iface,
|
interface: iface,
|
||||||
state: normalizeBfdState(s["state"]),
|
state: strictState && !s["state"]?.trim() ? "Unknown" : normalizeBfdState(s["state"]),
|
||||||
uptime: s["uptime"] ?? null,
|
uptime: s["uptime"] ?? null,
|
||||||
multihop: s["multihop"] === "true",
|
multihop: s["multihop"] === "true",
|
||||||
multiplier: parseInt(s["multiplier"] ?? "3") || 3,
|
multiplier: parseInt(s["multiplier"] ?? "3") || 3,
|
||||||
@@ -554,7 +554,7 @@ const catalogDefaults: OspfCatalogDependencies = {
|
|||||||
readCatalog: async server => {
|
readCatalog: async server => {
|
||||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await readOspfCatalogRaw(MikrotikClient.fromServer(server))
|
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await readOspfCatalogRaw(MikrotikClient.fromServer(server))
|
||||||
const areaMap = buildAreaMap(areas)
|
const areaMap = buildAreaMap(areas)
|
||||||
return { neighbors: parseNeighbors(server, neighbors, areaMap), interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap), instances: parseInstances(server, instances), bfdSessions: parseBfdSessions(server, bfdSessions).map((session, index) => ({ ...session, state: bfdSessions[index].state?.trim() ? session.state : "Unknown" })), routes: parseOspfRoutes(server, ipRoutes) }
|
return { neighbors: parseNeighbors(server, neighbors, areaMap), interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap), instances: parseInstances(server, instances), bfdSessions: parseBfdSessions(server, bfdSessions, true), routes: parseOspfRoutes(server, ipRoutes) }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalogDefaults): FastifyPluginAsyncZod {
|
export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalogDefaults): FastifyPluginAsyncZod {
|
||||||
|
|||||||
@@ -26,11 +26,18 @@
|
|||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.16, cost OSPF по областям; далее показатели/URL/layout |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.17, регрессия OSPF/BGP; далее nullable показатели/URL/layout |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 10.17 — strict BFD и контрольная регрессия (2026-09-30)
|
||||||
|
|
||||||
|
- Strict-state режим BFD parser закреплён прямыми backend fixtures: missing/blank → Unknown, known up/down сохраняются; legacy missing → Down остаётся совместимым. Изменений сетевых правил нет.
|
||||||
|
- Восемь OSPF source E2E, один freshness E2E, весь test:live-maps, backend build и текущий production frontend (34 страницы) прошли. Первоначальный запуск backend регрессии дал системный ENOMEM в tsx до тестов; повторный запуск прошёл. Lint без ошибок, четыре прежних warning (два frontend, два backend).
|
||||||
|
- В [карте этапа 10](ux-ui-stage-10.md) сохранён точный порядок nullable parser/DTO/Zod/UI/readiness/tests и таблица подменённых значений. Приоритет следующего блока: не подменять missing счётчики/таймеры и route metric нулями/distance; затем URL и визуальная приёмка.
|
||||||
|
- Использовано 98% пятичасового окна. Этап 10 остаётся в работе, стадии 11–12 не начаты; реальные RouterOS/DB/JWT недоступны. Локальный коммит без push.
|
||||||
|
|
||||||
## Блок 10.16 — cost в области OSPF (2026-09-30)
|
## Блок 10.16 — cost в области OSPF (2026-09-30)
|
||||||
|
|
||||||
- Индекс cost учитывает устройство, интерфейс и areaId. Одинаковое имя в разных областях не подставляет чужую стоимость. Конфликтующие шаблоны одного ключа и cost вне диапазона 1..65535 дают null; одинаковые подтверждённые значения сохраняются.
|
- Индекс cost учитывает устройство, интерфейс и areaId. Одинаковое имя в разных областях не подставляет чужую стоимость. Конфликтующие шаблоны одного ключа и cost вне диапазона 1..65535 дают null; одинаковые подтверждённые значения сохраняются.
|
||||||
|
|||||||
@@ -104,3 +104,19 @@ Lock/readiness, существующее mm:servers:write, валидация п
|
|||||||
## 10.16. Cost в области OSPF
|
## 10.16. Cost в области OSPF
|
||||||
|
|
||||||
Ключ включает устройство/interface/areaId. Конфликты шаблонов и недопустимый cost остаются неизвестными. 4 unit, 7 E2E и TS прошли; алгоритм оптимизации не изменён.
|
Ключ включает устройство/interface/areaId. Конфликты шаблонов и недопустимый cost остаются неизвестными. 4 unit, 7 E2E и TS прошли; алгоритм оптимизации не изменён.
|
||||||
|
|
||||||
|
## Следующий блок: nullable показатели строгого каталога
|
||||||
|
|
||||||
|
| Поля | Текущее поведение | Требуемый результат и проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| BFD packets-rx/tx, state-changes | Отсутствие/ошибка превращается в 0 | null при отсутствии/ошибке, подтверждённый 0 сохраняется; сумма неизвестна при неполном составе |
|
||||||
|
| BFD multiplier | Отсутствие превращается в 3 | null без фактического значения; диапазон проверить отдельно от состояния |
|
||||||
|
| BFD tx/rx/hold timers | parseMs возвращает 0, UI скрывает даже известный 0 | Разделить null/0; проверить RouterOS duration, не подменять actual-tx желаемым интервалом |
|
||||||
|
| Neighbor priority/state-changes | Подстановка 1/0 | null при неизвестности, priority=0 сохраняется; таблица показывает «—» |
|
||||||
|
| Interface cost/priority/hello/dead | Подстановка 10/1/0 | Строгий parser отдельно от legacy/optimizer; неизвестный cost запрещает предложение оптимизации по неполному каталогу |
|
||||||
|
| Route metric | ospf-metric подменяется distance | Distance не является OSPF cost; отсутствие metric остаётся null, отдельная проверка legacy совместимости |
|
||||||
|
| Instance version/router-id | Подстановка/default значения | Сверить реальные признаки RouterOS; не дописывать измерения без источника |
|
||||||
|
|
||||||
|
Порядок: отдельные catalog DTO и strict parser → backend fixtures (missing/zero/malformed/known) → Zod → таблицы/сводка/топология → readiness операций → API E2E → профильная сборка. Старые endpoints и оптимизатор сохраняют совместимость, пока отдельный контракт не проверен. Значения cache при отказе помечаются последним снимком; времена чтения/записи остаются раздельными.
|
||||||
|
|
||||||
|
После nullable блока: URL устройства/вкладки/поиска с back/reload и отсутствующим устройством; при переходе по истории во время write обязательно освободить lock прежней операции только в соответствующем контексте. Затем одна desktop/mobile схема и скриншоты; не считать OSPF принятым до этих проверок.
|
||||||
Reference in new issue
Block a user