diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 66fabebb..67e4f4a8 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -260,3 +260,5 @@ REST запрос → ожидание → подтверждённый output / Завершён 11Б.3.4.4.4: 6 таблиц/counters purge в общей транзакции, отдельные partition/listener/event warnings и nullable размер файлов. DI/HTTP/API/typecheck/build/lint прошли; реальный PG purge не запускался. Следующий шаг 11Б.3.4.4.5 — форма NetFlow и подтверждённые/частичные/неизвестные операции. Завершён 11Б.3.4.4.5: форма NetFlow known/dirty/rights/lock, selective PUT, partial outcomes и review после unknown POST; 12 E2E/form unit/typecheck/lint/production build прошли. Далее 11Б.3.4.4.6 — source/adaptive приёмка, затем общий контракт IP/CIDR/DNS. + +Завершён 11Б.3.4.4.6: 10 NetFlow backend source E2E, 3 responsive, 6 снимков (320 и ПК просмотрены), регрессия GeoIP/collector прошли. Далее 11Б.3.4.4.7: общий IP/CIDR/DNS ввод/фактический порт и отказ конкурентному purge вместо очереди. Затем приёмка NetFlow и Evolution. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 6243aba9..700ebd09 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.4.4.5; status/history/KPI, strict settings API основа и защита каталога Evolution проверены; далее readers/dirty формы | +| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.4.4.6; scheduler и GeoIP приняты локально; NetFlow API/backend/форма/source/adaptive проверены, далее общий сетевой ввод и Evolution | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,13 @@ +## Блок 11Б.3.4.4.6 — контекст и адаптивность NetFlow (2026-10-03) + +- 10 source E2E проверили поздние GET/PUT/keys/purge/export, success/error старого backend не меняют настройки/ошибки/receipt/Sheet нового backend. Конфигурация :3101/local отдельна от основного :3100/same-origin. +- 3 responsive tests на 1440/390/320 прошли; сохранены 6 снимков верхней части и действий docs/ux-ui/screens/netflow*.png. Просмотрены 320 верх/действия и 1440 верх: подписи читаемы, кнопки переносятся, destructive секция на телефоне вертикальная, body не переполняется. Допуск 2px относится только к границе Frame. +- Повторная группа 16 E2E (3 responsive + 10 GeoIP + 3 collector/probes) прошла. Source lint прошёл; production/typecheck приняты в 11Б.3.4.4.5. Никаких реальных private keys, PG purge, UDP/Portal/RouterOS в снимках и fixture checks нет. +- Далее 11Б.3.4.4.7: общий IP/CIDR/DNS ввод backend/frontend и инструкции с фактическим портом; также выявлено, что текущий advisory lock purge ставит второе удаление в очередь вместо 409 — требуется отказ конкурентному purge. После этих блоков локальная приёмка NetFlow, затем Evolution. + ## Блок 11Б.3.4.4.5 — достоверная форма NetFlow (2026-10-03) - Форма использует известные DTO, пустые unknown значения, время получения, последние данные и раздельные read/operation outcomes. Dirty хранится по полям, GET обновляет untouched и не стирает правки, PUT отправляет только изменённое без parseInt/default/clamp. Нулевой порог и false сохранены; включение порога требует явного допустимого значения. diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md index 8a5b605b..dcd72821 100644 --- a/docs/ux-ui-stage-11b.md +++ b/docs/ux-ui-stage-11b.md @@ -188,3 +188,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S Завершён 11Б.3.4.4.4: 6 таблиц/counters purge в общей транзакции, отдельные partition/listener/event warnings и nullable размер файлов. DI/HTTP/API/typecheck/build/lint прошли; реальный PG purge не запускался. Следующий шаг 11Б.3.4.4.5 — форма NetFlow и подтверждённые/частичные/неизвестные операции. Завершён 11Б.3.4.4.5: форма NetFlow known/dirty/rights/lock, selective PUT, partial outcomes и review после unknown POST; 12 E2E/form unit/typecheck/lint/production build прошли. Далее 11Б.3.4.4.6 — source/adaptive приёмка, затем общий контракт IP/CIDR/DNS. + +Завершён 11Б.3.4.4.6: 10 NetFlow backend source E2E, 3 responsive, 6 снимков (320 и ПК просмотрены), регрессия GeoIP/collector прошли. Далее 11Б.3.4.4.7: общий IP/CIDR/DNS ввод/фактический порт и отказ конкурентному purge вместо очереди. Затем приёмка NetFlow и Evolution. diff --git a/docs/ux-ui/screens/netflow-1440.png b/docs/ux-ui/screens/netflow-1440.png new file mode 100644 index 00000000..ab52d4a4 Binary files /dev/null and b/docs/ux-ui/screens/netflow-1440.png differ diff --git a/docs/ux-ui/screens/netflow-320.png b/docs/ux-ui/screens/netflow-320.png new file mode 100644 index 00000000..1b35eb56 Binary files /dev/null and b/docs/ux-ui/screens/netflow-320.png differ diff --git a/docs/ux-ui/screens/netflow-390.png b/docs/ux-ui/screens/netflow-390.png new file mode 100644 index 00000000..c12174fd Binary files /dev/null and b/docs/ux-ui/screens/netflow-390.png differ diff --git a/docs/ux-ui/screens/netflow-actions-1440.png b/docs/ux-ui/screens/netflow-actions-1440.png new file mode 100644 index 00000000..86c25d4d Binary files /dev/null and b/docs/ux-ui/screens/netflow-actions-1440.png differ diff --git a/docs/ux-ui/screens/netflow-actions-320.png b/docs/ux-ui/screens/netflow-actions-320.png new file mode 100644 index 00000000..dfe55b5a Binary files /dev/null and b/docs/ux-ui/screens/netflow-actions-320.png differ diff --git a/docs/ux-ui/screens/netflow-actions-390.png b/docs/ux-ui/screens/netflow-actions-390.png new file mode 100644 index 00000000..3cfa69eb Binary files /dev/null and b/docs/ux-ui/screens/netflow-actions-390.png differ diff --git a/playwright.config.ts b/playwright.config.ts index 485bc0d3..d9e77f90 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "@playwright/test" export default defineConfig({ testDir: "./tests/e2e", - testIgnore: ["**/geoip-context.spec.ts", "**/data-collection-context.spec.ts", "**/backups-context.spec.ts", "**/certificates-context.spec.ts", "**/containers-context.spec.ts", "**/users-context.spec.ts", "**/probes-context.spec.ts", "**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"], + testIgnore: ["**/netflow-context.spec.ts", "**/geoip-context.spec.ts", "**/data-collection-context.spec.ts", "**/backups-context.spec.ts", "**/certificates-context.spec.ts", "**/containers-context.spec.ts", "**/users-context.spec.ts", "**/probes-context.spec.ts", "**/terminal-context.spec.ts", "**/network-map-context.spec.ts", "**/route-optimizer-context.spec.ts", "**/recursive-routes-context.spec.ts", "**/ospf-context.spec.ts", "**/bgp-context.spec.ts", "**/firewall-context.spec.ts", "**/filters-context.spec.ts", "**/ipsec-context.spec.ts", "**/gre-context.spec.ts", "**/vxlan-context.spec.ts"], fullyParallel: false, workers: 1, retries: 0, diff --git a/playwright.netflow-context.config.ts b/playwright.netflow-context.config.ts new file mode 100644 index 00000000..5b47eae2 --- /dev/null +++ b/playwright.netflow-context.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "@playwright/test" +import base from "./playwright.config" +const server = base.webServer +if (!server || Array.isArray(server)) throw new Error("Ожидается один локальный сервер") +export default defineConfig({ ...base, use: { ...base.use, baseURL: "http://127.0.0.1:3101" }, testIgnore: [], testMatch: "netflow-context.spec.ts", webServer: { ...server, command: "node node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3101", url: "http://127.0.0.1:3101", env: { ...server.env, NEXT_PUBLIC_BACKEND_URL: "local" } } }) diff --git a/tests/e2e/netflow-context.spec.ts b/tests/e2e/netflow-context.spec.ts new file mode 100644 index 00000000..6e8b5478 --- /dev/null +++ b/tests/e2e/netflow-context.spec.ts @@ -0,0 +1,28 @@ +import {test,expect,type Page} from "@playwright/test" +import {keyedNetflowSettings as settings} from "./fixtures/netflow-settings" +const headers={"access-control-allow-origin":"*","access-control-allow-methods":"GET,POST,PUT,OPTIONS","access-control-allow-headers":"Content-Type,Authorization"} +async function setup(page:Page){ + await page.route("**/health",r=>r.fulfill({headers,json:{ok:true}})) + await page.route("**/api/**",r=>{const url=new URL(r.request().url());return r.fulfill(url.pathname==="/api/auth/config"?{headers,json:{required:false}}:url.pathname==="/api/traffic/flow/settings"?{headers,json:{...settings,flowListenPort:url.port==="8101"?4801:4739}}:{headers,status:503,json:{error:"outside NetFlow fixture"}})}) +} +for(const action of ["read","put","keys","purge","export"] as const)for(const fails of [false,true])test("NetFlow: поздний ответ старого backend/"+action+"/"+fails,async({page})=>{ + await setup(page);let started=false,completed=false,release!:()=>void;const gate=new Promise(resolve=>release=resolve) + const path=action==="keys"?"/api/traffic/flow/settings/generate-keys":action==="purge"?"/api/traffic/flow/purge":action==="export"?"/api/traffic/flow/host-files":"/api/traffic/flow/settings" + await page.route("**"+path,async r=>{ + if(new URL(r.request().url()).port!=="8000"||(action==="put"?r.request().method()!=="PUT":action==="read"?r.request().method()!=="GET":false))return r.fallback() + started=true;await gate + const old={...settings,flowListenPort:4740} + const json=action==="read"?old:action==="put"?{ok:true,settings:old}:action==="keys"?{ok:true,created:true,publicKey:old.hostPublicKey,settings:old}:action==="export"?{files:[{id:"wg",label:"WG",filename:"wg.conf",code:"fixture-old"}]}:{ok:true,deleted:{buckets:21,minuteStats:0,minuteDims:0,dailyDims:0},fileBytesBefore:null,fileBytesAfter:null,vacuumed:false} + try{await r.fulfill(fails?{headers,status:503,json:{error:"СТАРАЯ ОШИБКА"}}:{headers,json})}catch{}finally{completed=true} + }) + try{ + await page.goto("/data-collection");const field=page.getByRole("textbox",{name:"UDP IPFIX",exact:true}) + if(action!=="read")await expect(field).toHaveValue("4739") + if(action==="put"){await field.fill("4740");await page.getByRole("button",{name:"Сохранить NetFlow",exact:true}).click()} + if(action==="keys")await page.getByRole("button",{name:"Ключи хоста",exact:true}).click() + if(action==="export")await page.getByRole("button",{name:"Скачать файлы хоста NetFlow",exact:true}).click() + if(action==="purge"){await page.getByRole("button",{name:"Сбросить",exact:true}).click();await page.getByRole("alertdialog").getByRole("button",{name:"Сбросить",exact:true}).click()} + await expect.poll(()=>started).toBe(true);await page.goto("/settings");const url=page.getByPlaceholder("http://localhost:8000");await url.fill("http://localhost:8101");await url.press("Enter");await page.goto("/data-collection");await expect(field).toHaveValue("4801");release();await expect.poll(()=>completed).toBe(true) + await expect(field).toHaveValue("4801");await expect(page.getByText(/СТАРАЯ ОШИБКА/)).toHaveCount(0);await expect(page.getByRole("status").filter({hasText:/подтверждено|подтверждён/})).toHaveCount(0);await expect(page.getByRole("dialog",{name:"Файлы для хоста Docker MM",exact:true})).toHaveCount(0);await expect(page.getByRole("button",{name:"Ключи хоста",exact:true})).toBeEnabled() + }finally{release()} +}) diff --git a/tests/e2e/netflow-responsive.spec.ts b/tests/e2e/netflow-responsive.spec.ts new file mode 100644 index 00000000..06c091d7 --- /dev/null +++ b/tests/e2e/netflow-responsive.spec.ts @@ -0,0 +1,37 @@ +import { test, expect, type Page } from "@playwright/test" +import { geoip } from "./fixtures/geoip" +import { keyedNetflowSettings } from "./fixtures/netflow-settings" +const common = { enabled: true, intervalSec: 300, retentionDays: 14, lastCollectedAt: null, lastDurationMs: 0, lastError: null } +const values: Record = { + "/api/traffic/settings": { ...common, intervalSec: 45 }, + "/api/servers-api-ping/settings": { ...common, enabled: false, intervalSec: 120 }, + "/api/uptime/settings": { ...common, resourcesEnabled: true, pingEnabled: false, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 }, + "/api/internet-path/settings": common, + "/api/certificates/renew-settings": { enabled: false, intervalSec: 21600, renewBeforeDays: 30 }, +} +async function setup(page: Page) { + 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/geoip") return r.fulfill({ json: geoip }) + if (path === "/api/scheduler/status") return r.fulfill({ json: { jobs: [] } }) + if (path === "/api/scheduler/runs") return r.fulfill({ json: { runs: [] } }) + if (path === "/api/traffic/flow/settings") return r.fulfill({ json: {...keyedNetflowSettings,publicEndpoint:"collector.example.test"} }) + if (values[path]) return r.fulfill({ json: values[path] }) + return r.fulfill({ status: 503, json: { error: "outside settings fixture" } }) + }) +} + + +for (const width of [1440, 390, 320]) test("NetFlow: адаптивность/" + width, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }); await setup(page); await page.goto("/data-collection") + const section = page.getByText("Traffic Flow / NetFlow (IPFIX)", { exact: true }).locator("xpath=ancestor::*[@data-slot='frame'][1]") + await expect(page.getByRole("textbox", { name: "UDP IPFIX", exact: true })).toHaveValue("4739") + await page.getByText("Traffic Flow / NetFlow (IPFIX)", {exact:true}).scrollIntoViewIfNeeded() + expect(await section.evaluate(element => element.scrollWidth <= element.clientWidth + 2)).toBe(true) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) + await page.screenshot({ path: "docs/ux-ui/screens/netflow-" + width + ".png", fullPage: true }) + await page.getByRole("button",{name:"Сохранить NetFlow",exact:true}).scrollIntoViewIfNeeded() + await page.screenshot({path:"docs/ux-ui/screens/netflow-actions-"+width+".png",fullPage:true}) +})