fix(netflow): подтверждать выбранный срез перед запросами

This commit is contained in:
Denozordec committed 2026-10-11 18:16:29 +07:00
1 parent 64df472202
commit 0c14da190b
13 files changed
+338 -21

No files matched your search

+11 -4
View File
@@ -832,7 +832,14 @@ export default function TrafficPage() {
serverId: selectedId,
iface: selectedIface,
})
const flowLiveEnabled = isLive && effectiveMode === "flows" && Boolean(selectedId) && range === "5m"
const confirmedFlowSelection = isLive && Boolean(currentFlows) && !liveError && (
range === "30d"
? /^[1-9]\d*$/.test(selectedId) && Number.isSafeInteger(Number(selectedId)) && Boolean(currentFlows?.monthlyServers?.some(server => String(server.id) === selectedId))
: flowScope === "servers"
? /^[1-9]\d*$/.test(selectedId) && Number.isSafeInteger(Number(selectedId)) && flowExporters.some(card => card.id === selectedId)
: Boolean(selectedId) && flowClients.some(card => card.id === selectedId)
)
const flowLiveEnabled = confirmedFlowSelection && effectiveMode === "flows" && range === "5m"
const { sample: flowLiveSample, error: flowLiveError, receivedAt: flowLiveReceivedAt } = useFlowLive({
enabled: flowLiveEnabled,
backendUrl,
@@ -914,9 +921,9 @@ export default function TrafficPage() {
const analyticsKey = JSON.stringify([flowKey, selectedId, flowIface, flowDedup, flowExcludeMesh, flowExcludeOverlay, analyticsRetry, range === "30d" ? flowMonth : null])
const currentAnalytics = analyticsResult?.key === analyticsKey ? analyticsResult : null
const analyticsLoading = isLive && effectiveMode === "flows" && Boolean(selectedId) && (range !== "30d" || flowMonthValid) && !currentAnalytics
const analyticsLoading = confirmedFlowSelection && effectiveMode === "flows" && (range !== "30d" || flowMonthValid) && !currentAnalytics
useEffect(() => {
if (!isLive || effectiveMode !== "flows" || !selectedId || (range === "30d" && !flowMonthValid)) return
if (!confirmedFlowSelection || effectiveMode !== "flows" || (range === "30d" && !flowMonthValid)) return
let cancelled = false
const request = range === "30d"
? getFlowMonthly(backendUrl, { month: flowMonth, serverId: flowScope === "servers" ? selectedId : undefined }).then(monthly => ({ monthly }))
@@ -931,7 +938,7 @@ export default function TrafficPage() {
if (!cancelled) setAnalyticsResult({ key: analyticsKey, error: e instanceof Error ? e.message : "Не удалось загрузить аналитику" })
})
return () => { cancelled = true }
}, [isLive, effectiveMode, selectedId, range, flowScope, flowIface, flowDedup, flowExcludeMesh, flowExcludeOverlay, backendUrl, analyticsKey, flowMonth, flowMonthValid])
}, [confirmedFlowSelection, isLive, effectiveMode, selectedId, range, flowScope, flowIface, flowDedup, flowExcludeMesh, flowExcludeOverlay, backendUrl, analyticsKey, flowMonth, flowMonthValid])
useEffect(() => {
if (!isLive) return
+1 -1
View File
@@ -20,7 +20,7 @@
"test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts",
"test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts && npx tsx src/routes/wireguard-import.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts",
"test:traffic-flow": "tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
"test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
"test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
"test:pg": "tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts",
"test:recursive-routes": "tsx src/routes/recursive-routes.test.ts && tsx src/services/recursive-route-catalog.test.ts && tsx src/services/recursive-apply.test.ts",
@@ -0,0 +1,29 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import { serializerCompiler, validatorCompiler } from "@fastify/type-provider-zod"
import trafficFlowRoutes, { resetFlowLiveSlotsForTests, tryAcquireFlowLiveSlot, releaseFlowLiveSlot } from "./traffic-flow.js"
import { closePool } from "../db/index.js"
const app = Fastify({ logger: false })
app.setValidatorCompiler(validatorCompiler)
app.setSerializerCompiler(serializerCompiler)
await app.register(trafficFlowRoutes, { prefix: "/api" })
resetFlowLiveSlotsForTests()
try {
for (const id of ["srv1", "1suffix", "-1", "0", "1.5", "1e3", "9007199254740992"]) {
for (const endpoint of ["analytics", "monthly", "live"]) {
const response = await app.inject({ method: "GET", url: `/api/traffic/flow/${endpoint}?serverId=${encodeURIComponent(id)}&month=2026-10` })
assert.equal(response.statusCode, 400, `${endpoint}/${id}`)
assert.match(response.body, /Некорректный идентификатор сервера/)
}
}
// Failed query validation must not consume the bounded live subscription quota.
for (let i = 0; i < 4; i++) assert.equal(tryAcquireFlowLiveSlot(), true)
assert.equal(tryAcquireFlowLiveSlot(), false)
for (let i = 0; i < 4; i++) releaseFlowLiveSlot()
console.log("traffic-flow-query.test.ts: 21 invalid scoped requests rejected; SSE slots retained")
} finally {
resetFlowLiveSlotsForTests()
await app.close()
await closePool()
}
+5 -3
View File
@@ -2,6 +2,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { registerDatabaseStreamAbort } from "../plugins/database-request-gate.js"
import type { FastifyReply, FastifyRequest } from "fastify"
import { env } from "../config.js"
import { parseDbServerId } from "../utils/server-id.js"
import {
trafficFlowOverlayRequestSchema,
} from "@mmapp/contracts/traffic-flow"
@@ -64,8 +65,9 @@ function rangeToMinutes(range: string | undefined): number {
function parseId(raw: unknown): number | undefined {
if (raw == null || raw === "") return undefined
const n = Number.parseInt(String(raw), 10)
return Number.isFinite(n) ? n : undefined
const n = parseDbServerId(raw)
if (n === null || !Number.isSafeInteger(n)) throw Object.assign(new Error("Некорректный идентификатор сервера"), { statusCode: 400 })
return n
}
function parseDedup(raw: unknown): boolean {
@@ -236,10 +238,10 @@ const trafficFlowRoutes: FastifyPluginAsyncZod = async (app) => {
})
app.get("/traffic/flow/live", async (req, reply) => {
const query = analyticsQuery(req)
if (!tryAcquireFlowLiveSlot()) {
return reply.status(429).send({ error: "Слишком много live-подписок" })
}
const query = analyticsQuery(req)
const liveQuery = {
serverId: query.serverId,
userId: query.userId,
+14 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
@@ -696,7 +696,7 @@ flowchart LR
## 12.5.77 — основной блок IPsec (2026-10-09)
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
@@ -1589,3 +1589,15 @@ HTTP analytics/monthly раньше принимались как typed JSON б
Frontend ux-http147 развёрнут, backend ux-labels146 сохранён. Настоящие analytics/monthly HTTP200 текущего backend прошли схемы; month2026-10 совпал с запросом. [Live PC/mobile smoke](ux-ui/acceptance/traffic-http-validation-smoke-2026-10-11.json). Это contract compatibility/read-only smoke с текущими lab data, не новый populated WAN test. Rollback image/env сохранены, device/password changes нет.
Следующий148 — не запускать scoped flow HTTP/SSE до подтверждённого выбранного элемента каталога и не превращать malformed serverId в aggregate request.12.5 открыт.
## 12.5.148 — подтверждённый срез потоков без агрегатной подмены (2026-10-11)
Исправлено продолжение initial srv1 defect для flow mode. Scoped analytics/SSE теперь ждут выбранный элемент current provider/range/scope catalog. Server ID обязан быть positive safe numeric и присутствовать в exporters/monthly catalog, user ID — в confirmed clients. Catalog pending/error не запускает запрос для guessed/old selection; analytics loading не висит после catalog failure. Monthly history сохраняет supported non-exporting/disabled catalog entries, device polling не добавлено.
Backend больше не извлекает numeric prefix из1suffix и не превращает srv1/invalid/unsafe IDs в omitted filter/all-servers aggregate. Возвращается400 Некорректный идентификатор сервера. Missing/empty optional filter сохраняет aggregate semantics. Query validation выполняется до acquisition MAX4 SSE slots, поэтому malformed requests не съедают live quota.
21 invalid query cases (7 IDs×analytics/monthly/live) прошли source/candidate; full4 slots остались доступны. Новый тест сначала импортировал несуществующий non-scoped package; исправлен на existing @fastify/type-provider-zod без установки dependencies, проверки затем passed. Backend test включён в обычную traffic-flow series.36 E2E одним запуском passed: delayed/error catalog без any scoped request, затем only confirmed id1; existing flow/SSE/races/month/DTO/IPv6 labels. ESLint/source-only types/both production builds и984 emitted backend outputs comparison passed.
Backend/frontend ux-scope148 развёрнуты, health healthy. Real API9 malformed cases400 и2 valid scoped cases200; [receipt](ux-ui/acceptance/flow-scoped-query-2026-10-11.json), [PC/mobile readonly smoke](ux-ui/acceptance/traffic-flow-scope-smoke-2026-10-11.json). No native changes/password changes/schema/theme changes; rollback env/images сохранены. Populated native145 не повторялся без необходимости.
Следующий149 — строгий month input и calendar bounds: invalid month не должен подменяться текущим или выдаваться за подтверждённый zero report.12.5 открыт.
+3 -3
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-10. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-10
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
@@ -371,6 +371,6 @@ flowchart LR
## 12.5.77 — основной блок IPsec (2026-10-09)
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
+14 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -36,7 +36,19 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–147 и ограничения ниже. Runtime backend ux-labels146/frontend ux-http147. |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–148 и ограничения ниже. Runtime backend/frontend ux-scope148. |
## 12.5.148 — подтверждённый срез потоков без агрегатной подмены (2026-10-11)
Исправлено продолжение initial srv1 defect для flow mode. Scoped analytics/SSE теперь ждут выбранный элемент current provider/range/scope catalog. Server ID обязан быть positive safe numeric и присутствовать в exporters/monthly catalog, user ID — в confirmed clients. Catalog pending/error не запускает запрос для guessed/old selection; analytics loading не висит после catalog failure. Monthly history сохраняет supported non-exporting/disabled catalog entries, device polling не добавлено.
Backend больше не извлекает numeric prefix из1suffix и не превращает srv1/invalid/unsafe IDs в omitted filter/all-servers aggregate. Возвращается400 Некорректный идентификатор сервера. Missing/empty optional filter сохраняет aggregate semantics. Query validation выполняется до acquisition MAX4 SSE slots, поэтому malformed requests не съедают live quota.
21 invalid query cases (7 IDs×analytics/monthly/live) прошли source/candidate; full4 slots остались доступны. Новый тест сначала импортировал несуществующий non-scoped package; исправлен на existing @fastify/type-provider-zod без установки dependencies, проверки затем passed. Backend test включён в обычную traffic-flow series.36 E2E одним запуском passed: delayed/error catalog без any scoped request, затем only confirmed id1; existing flow/SSE/races/month/DTO/IPv6 labels. ESLint/source-only types/both production builds и984 emitted backend outputs comparison passed.
Backend/frontend ux-scope148 развёрнуты, health healthy. Real API9 malformed cases400 и2 valid scoped cases200; [receipt](ux-ui/acceptance/flow-scoped-query-2026-10-11.json), [PC/mobile readonly smoke](ux-ui/acceptance/traffic-flow-scope-smoke-2026-10-11.json). No native changes/password changes/schema/theme changes; rollback env/images сохранены. Populated native145 не повторялся без необходимости.
Следующий149 — строгий month input и calendar bounds: invalid month не должен подменяться текущим или выдаваться за подтверждённый zero report.12.5 открыт.
## 12.5.147 — подтверждённые HTTP данные аналитики и месяца (2026-10-11)
+15 -3
View File
@@ -1,8 +1,8 @@
# Оставшаяся внешняя приёмка — 2026-10-11
Это актуальная матрица после 12.5.147. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Это актуальная матрица после 12.5.148. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-labels146`, frontend `ux-http147`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-scope148`, frontend `ux-scope148`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
| Раздел | Подтверждено | Осталось / условие завершения |
| --- | --- | --- |
@@ -18,7 +18,7 @@
| Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята |
| Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты |
| Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, native IPv6 ULA flow pipeline passed145 (IPv4 transport), public IPv6 WAN/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения |
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards148; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar149; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
@@ -504,3 +504,15 @@ HTTP analytics/monthly раньше принимались как typed JSON б
Frontend ux-http147 развёрнут, backend ux-labels146 сохранён. Настоящие analytics/monthly HTTP200 текущего backend прошли схемы; month2026-10 совпал с запросом. [Live PC/mobile smoke](ux-ui/acceptance/traffic-http-validation-smoke-2026-10-11.json). Это contract compatibility/read-only smoke с текущими lab data, не новый populated WAN test. Rollback image/env сохранены, device/password changes нет.
Следующий148 — не запускать scoped flow HTTP/SSE до подтверждённого выбранного элемента каталога и не превращать malformed serverId в aggregate request.12.5 открыт.
## 12.5.148 — подтверждённый срез потоков без агрегатной подмены (2026-10-11)
Исправлено продолжение initial srv1 defect для flow mode. Scoped analytics/SSE теперь ждут выбранный элемент current provider/range/scope catalog. Server ID обязан быть positive safe numeric и присутствовать в exporters/monthly catalog, user ID — в confirmed clients. Catalog pending/error не запускает запрос для guessed/old selection; analytics loading не висит после catalog failure. Monthly history сохраняет supported non-exporting/disabled catalog entries, device polling не добавлено.
Backend больше не извлекает numeric prefix из1suffix и не превращает srv1/invalid/unsafe IDs в omitted filter/all-servers aggregate. Возвращается400 Некорректный идентификатор сервера. Missing/empty optional filter сохраняет aggregate semantics. Query validation выполняется до acquisition MAX4 SSE slots, поэтому malformed requests не съедают live quota.
21 invalid query cases (7 IDs×analytics/monthly/live) прошли source/candidate; full4 slots остались доступны. Новый тест сначала импортировал несуществующий non-scoped package; исправлен на existing @fastify/type-provider-zod без установки dependencies, проверки затем passed. Backend test включён в обычную traffic-flow series.36 E2E одним запуском passed: delayed/error catalog без any scoped request, затем only confirmed id1; existing flow/SSE/races/month/DTO/IPv6 labels. ESLint/source-only types/both production builds и984 emitted backend outputs comparison passed.
Backend/frontend ux-scope148 развёрнуты, health healthy. Real API9 malformed cases400 и2 valid scoped cases200; [receipt](ux-ui/acceptance/flow-scoped-query-2026-10-11.json), [PC/mobile readonly smoke](ux-ui/acceptance/traffic-flow-scope-smoke-2026-10-11.json). No native changes/password changes/schema/theme changes; rollback env/images сохранены. Populated native145 не повторялся без необходимости.
Следующий149 — строгий month input и calendar bounds: invalid month не должен подменяться текущим или выдаваться за подтверждённый zero report.12.5 открыт.
+13 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-11: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Актуальное продолжение 2026-10-11: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -2894,3 +2894,15 @@ HTTP analytics/monthly раньше принимались как typed JSON б
Frontend ux-http147 развёрнут, backend ux-labels146 сохранён. Настоящие analytics/monthly HTTP200 текущего backend прошли схемы; month2026-10 совпал с запросом. [Live PC/mobile smoke](ux-ui/acceptance/traffic-http-validation-smoke-2026-10-11.json). Это contract compatibility/read-only smoke с текущими lab data, не новый populated WAN test. Rollback image/env сохранены, device/password changes нет.
Следующий148 — не запускать scoped flow HTTP/SSE до подтверждённого выбранного элемента каталога и не превращать malformed serverId в aggregate request.12.5 открыт.
## 12.5.148 — подтверждённый срез потоков без агрегатной подмены (2026-10-11)
Исправлено продолжение initial srv1 defect для flow mode. Scoped analytics/SSE теперь ждут выбранный элемент current provider/range/scope catalog. Server ID обязан быть positive safe numeric и присутствовать в exporters/monthly catalog, user ID — в confirmed clients. Catalog pending/error не запускает запрос для guessed/old selection; analytics loading не висит после catalog failure. Monthly history сохраняет supported non-exporting/disabled catalog entries, device polling не добавлено.
Backend больше не извлекает numeric prefix из1suffix и не превращает srv1/invalid/unsafe IDs в omitted filter/all-servers aggregate. Возвращается400 Некорректный идентификатор сервера. Missing/empty optional filter сохраняет aggregate semantics. Query validation выполняется до acquisition MAX4 SSE slots, поэтому malformed requests не съедают live quota.
21 invalid query cases (7 IDs×analytics/monthly/live) прошли source/candidate; full4 slots остались доступны. Новый тест сначала импортировал несуществующий non-scoped package; исправлен на existing @fastify/type-provider-zod без установки dependencies, проверки затем passed. Backend test включён в обычную traffic-flow series.36 E2E одним запуском passed: delayed/error catalog без any scoped request, затем only confirmed id1; existing flow/SSE/races/month/DTO/IPv6 labels. ESLint/source-only types/both production builds и984 emitted backend outputs comparison passed.
Backend/frontend ux-scope148 развёрнуты, health healthy. Real API9 malformed cases400 и2 valid scoped cases200; [receipt](ux-ui/acceptance/flow-scoped-query-2026-10-11.json), [PC/mobile readonly smoke](ux-ui/acceptance/traffic-flow-scope-smoke-2026-10-11.json). No native changes/password changes/schema/theme changes; rollback env/images сохранены. Populated native145 не повторялся без необходимости.
Следующий149 — строгий month input и calendar bounds: invalid month не должен подменяться текущим или выдаваться за подтверждённый zero report.12.5 открыт.
+14 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10
Сервер161.104.46.222: завершён12.5.147 — HTTP analytics/monthly проверяются existing contract schemas; malformed required DTO/negative series/wrong month дают error+retry, optional unknown metrics не прячут core и не становятся0.34 E2E across attempts/types/ESLint/build/live contracts+PC/mobile passed. Backend ux-labels146/frontend ux-http147. Далее148 — gate flow entity requests/SSE до confirmed catalog и reject invalid serverId без aggregate fallback.12.5 открыт.
Сервер161.104.46.222: завершён12.5.148 — scoped flow HTTP/SSE ждут confirmed catalog; invalid serverId получает400 без aggregate fallback/SSE slot leak.36 E2E,21 backend checks/source+candidate,984 outputs/types/ESLint/both builds,live API+PC/mobile passed. Backend/frontend ux-scope148. Далее149 — strict monthly calendar input/bounds, без подмены invalid month текущим/нулевым отчётом.12.5 открыт.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -14,7 +14,7 @@
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
| Образы приложения | mmapp-test/backend:ux-labels146 и mmapp-test/frontend:ux-http147; запущены |
| Образы приложения | mmapp-test/backend:ux-scope148 и mmapp-test/frontend:ux-scope148; запущены |
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
@@ -1320,3 +1320,15 @@ HTTP analytics/monthly раньше принимались как typed JSON б
Frontend ux-http147 развёрнут, backend ux-labels146 сохранён. Настоящие analytics/monthly HTTP200 текущего backend прошли схемы; month2026-10 совпал с запросом. [Live PC/mobile smoke](ux-ui/acceptance/traffic-http-validation-smoke-2026-10-11.json). Это contract compatibility/read-only smoke с текущими lab data, не новый populated WAN test. Rollback image/env сохранены, device/password changes нет.
Следующий148 — не запускать scoped flow HTTP/SSE до подтверждённого выбранного элемента каталога и не превращать malformed serverId в aggregate request.12.5 открыт.
## 12.5.148 — подтверждённый срез потоков без агрегатной подмены (2026-10-11)
Исправлено продолжение initial srv1 defect для flow mode. Scoped analytics/SSE теперь ждут выбранный элемент current provider/range/scope catalog. Server ID обязан быть positive safe numeric и присутствовать в exporters/monthly catalog, user ID — в confirmed clients. Catalog pending/error не запускает запрос для guessed/old selection; analytics loading не висит после catalog failure. Monthly history сохраняет supported non-exporting/disabled catalog entries, device polling не добавлено.
Backend больше не извлекает numeric prefix из1suffix и не превращает srv1/invalid/unsafe IDs в omitted filter/all-servers aggregate. Возвращается400 Некорректный идентификатор сервера. Missing/empty optional filter сохраняет aggregate semantics. Query validation выполняется до acquisition MAX4 SSE slots, поэтому malformed requests не съедают live quota.
21 invalid query cases (7 IDs×analytics/monthly/live) прошли source/candidate; full4 slots остались доступны. Новый тест сначала импортировал несуществующий non-scoped package; исправлен на existing @fastify/type-provider-zod без установки dependencies, проверки затем passed. Backend test включён в обычную traffic-flow series.36 E2E одним запуском passed: delayed/error catalog без any scoped request, затем only confirmed id1; existing flow/SSE/races/month/DTO/IPv6 labels. ESLint/source-only types/both production builds и984 emitted backend outputs comparison passed.
Backend/frontend ux-scope148 развёрнуты, health healthy. Real API9 malformed cases400 и2 valid scoped cases200; [receipt](ux-ui/acceptance/flow-scoped-query-2026-10-11.json), [PC/mobile readonly smoke](ux-ui/acceptance/traffic-flow-scope-smoke-2026-10-11.json). No native changes/password changes/schema/theme changes; rollback env/images сохранены. Populated native145 не повторялся без необходимости.
Следующий149 — строгий month input и calendar bounds: invalid month не должен подменяться текущим или выдаваться за подтверждённый zero report.12.5 открыт.
@@ -0,0 +1,88 @@
{
"date": "2026-10-11",
"block": "12.5.148",
"runtime": {
"backend": "ux-scope148",
"frontend": "ux-scope148",
"backendHealthy": true
},
"checks": {
"e2ePassed": 36,
"invalidBackendRequestsRejectedLocallyAndInCandidate": 21,
"invalidLiveSlotQuotaNotConsumed": true,
"sourceOnlyTypesPassed": true,
"changedFilesEslintPassed": true,
"backendFrontendBuildPassed": true,
"compiledBackendOutputsMatched": 984,
"liveApi": [
{
"endpoint": "analytics",
"id": "srv1",
"status": 400
},
{
"endpoint": "analytics",
"id": "1suffix",
"status": 400
},
{
"endpoint": "analytics",
"id": "9007199254740992",
"status": 400
},
{
"endpoint": "monthly",
"id": "srv1",
"status": 400
},
{
"endpoint": "monthly",
"id": "1suffix",
"status": 400
},
{
"endpoint": "monthly",
"id": "9007199254740992",
"status": 400
},
{
"endpoint": "live",
"id": "srv1",
"status": 400
},
{
"endpoint": "live",
"id": "1suffix",
"status": 400
},
{
"endpoint": "live",
"id": "9007199254740992",
"status": 400
},
{
"endpoint": "analytics?range=5m",
"serverId": 1,
"status": 200
},
{
"endpoint": "monthly?month=2026-10",
"serverId": 1,
"status": 200
}
]
},
"initialTestSetup": {
"importFailedBeforeTests": true,
"reason": "New test imported wrong non-scoped provider package; corrected to existing @fastify/type-provider-zod without installing dependencies"
},
"bounds": [
"Analytics/SSE requires selected entity in current backend/range/scope catalog; server IDs positive safe numeric; user IDs matched in confirmed client catalog",
"Monthly requires selected numeric server present in confirmed monthly catalog; historical disabled/non-exporting catalog entries retained",
"Metadata loading/error does not start scoped HTTP/SSE or leave analytics spinner pretending data are pending after catalog failure",
"Invalid serverId no longer becomes omitted/aggregate filter; omitted or empty optional ID retains existing aggregate semantics",
"SSE query validated before bounded slot acquisition; bad queries do not exhaust MAX4 subscriptions",
"Delayed/error catalog checks plus existing monthly/races/SSE/DTO/IPv6-label regression passed; no device/password/schema/theme changes",
"Live API nine invalid cases400/two valid cases200 and readonly PC/mobile smoke; no newly populated native flow test repeated"
]
}
@@ -0,0 +1,84 @@
{
"date": "2026-10-11",
"block": "12.5.148",
"runtime": {
"backend": "ux-scope148",
"frontend": "ux-scope148"
},
"checks": {
"readOnlyFrontendUnchanged": true,
"backendHealthy": true,
"backendProductionBuildPassed": true
},
"cases": [
{
"width": 1440,
"httpStatus": 200,
"api": [
{
"path": "/api/traffic/servers",
"status": 200
},
{
"path": "/api/traffic/users",
"status": 200
},
{
"path": "/api/traffic/bound-interfaces",
"status": 200
},
{
"path": "/api/traffic/servers/1/live",
"status": 200
},
{
"path": "/api/traffic/servers/1",
"status": 200
},
{
"path": "/api/traffic/servers/1/interfaces",
"status": 200
}
],
"pageErrors": [],
"noOverflow": true
},
{
"width": 320,
"httpStatus": 200,
"api": [
{
"path": "/api/traffic/bound-interfaces",
"status": 200
},
{
"path": "/api/traffic/servers",
"status": 200
},
{
"path": "/api/traffic/users",
"status": 200
},
{
"path": "/api/traffic/servers/1/live",
"status": 200
},
{
"path": "/api/traffic/servers/1",
"status": 200
},
{
"path": "/api/traffic/servers/1/interfaces",
"status": 200
}
],
"pageErrors": [],
"noOverflow": true
}
],
"bounds": [
"Read-only initial loading with real current lab catalog; no full NetFlow ingestion or throughput claim",
"Catalog failure and pending loading checked by controlled E2E; numeric live server details/interfaces/SSE enabled only after confirmed catalog ownership",
"Interface filter remains selected on range changes; backend source switch invalidates prior catalog ownership"
]
}
+47
View File
@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test'
for (const outcome of ['success', 'error'] as const) {
test(`Потоки: каталог ${outcome} блокирует неподтверждённые HTTP/SSE запросы`, async ({ page }) => {
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
const scoped: Array<{ path: string; serverId: string | null }> = []
const stats = { bpsNow: 0, bytes: 200, packets: 2, conversations: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', rxSeries: [], txSeries: [], applications: [], protocols: [], sources: [], destinations: [], interfaces: [], conversationsList: [], ifaces: [], live: false }
const card = { id: '1', name: 'Подтверждённый экспортёр', subtitle: '', site: 'TEST', country: '', status: 'online', rxNow: 0, txNow: 0, sessions: 1, bytes: 200, rxSeries: [], txSeries: [] }
await page.route('**/health', route => route.fulfill({ json: { ok: true } }))
await page.route('**/api/**', async route => {
const url = new URL(route.request().url()), path = url.pathname
if (['/api/traffic/flow/analytics', '/api/traffic/flow/monthly', '/api/traffic/flow/live'].includes(path)) scoped.push({ path, serverId: url.searchParams.get('serverId') })
if (path === '/api/traffic/flow/exporters') {
await gate
if (outcome === 'error') return route.fulfill({ status: 503, json: { error: 'Каталог экспортёров недоступен' } })
return route.fulfill({ json: { exporters: [card] } })
}
if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} })
const json = path === '/api/auth/config' ? { required: false }
: path === '/api/traffic/flow/analytics' ? stats
: path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 200, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true }
: path === '/api/traffic/servers' ? { servers: [] }
: path === '/api/traffic/users' ? { users: [] }
: path === '/api/traffic/flow/clients' ? { clients: [] }
: path.endsWith('/interfaces') ? { interfaces: [] }
: path === '/api/servers' ? [] : {}
return route.fulfill({ json })
})
await page.goto('/traffic')
const catalog = page.waitForRequest(r => new URL(r.url()).pathname === '/api/traffic/flow/exporters')
await page.getByRole('group', { name: 'Разделы трафика' }).getByRole('button', { name: 'Потоки', exact: true }).click()
await catalog
await expect(page.getByText('Загрузка потоков…', { exact: true })).toBeVisible()
await page.waitForTimeout(500)
expect(scoped).toEqual([])
release()
if (outcome === 'success') {
await expect.poll(() => scoped.some(r => r.path.endsWith('/analytics'))).toBe(true)
await expect.poll(() => scoped.some(r => r.path.endsWith('/live'))).toBe(true)
expect(scoped.every(r => r.serverId === '1')).toBe(true)
} else {
await expect(page.getByRole('alert').filter({ hasText: 'Потоки недоступны' })).toContainText('Каталог экспортёров недоступен')
expect(scoped).toEqual([])
}
})
}