fix(netflow): завершить локальную приёмку и объединить сценарий сброса
No files matched your search
@@ -32,8 +32,6 @@ import {
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database"
|
||||
import { purgeTrafficFlowData } from "@/shared/api/traffic-flow"
|
||||
import { formatFlowPurgeResult, NetflowPurgeConfirm } from "@/components/traffic/netflow-purge-dialog"
|
||||
import { toast } from "sonner"
|
||||
|
||||
interface ApiKey { id: string; name: string; prefix: string; created: string; last: string; scopes: string[] }
|
||||
@@ -150,8 +148,6 @@ export default function SettingsPage() {
|
||||
const [dbRestoreBusy, setDbRestoreBusy] = useState(false)
|
||||
const [dbRestoreFile, setDbRestoreFile] = useState<File | null>(null)
|
||||
const [dbRestoreDialogOpen, setDbRestoreDialogOpen] = useState(false)
|
||||
const [dbPurgeOpen, setDbPurgeOpen] = useState(false)
|
||||
const [dbPurgeBusy, setDbPurgeBusy] = useState(false)
|
||||
|
||||
// notifications
|
||||
const [notifEmail, setNotifEmail] = useState(true)
|
||||
@@ -270,19 +266,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
}, [backendUrl, dbRestoreFile, systemDbAvailable])
|
||||
|
||||
const handleNetflowPurgeConfirm = useCallback(async () => {
|
||||
if (!systemDbAvailable) return
|
||||
setDbPurgeBusy(true)
|
||||
try {
|
||||
const result = await purgeTrafficFlowData(backendUrl)
|
||||
toast.success(formatFlowPurgeResult(result))
|
||||
setDbPurgeOpen(false)
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Не удалось сбросить NetFlow")
|
||||
} finally {
|
||||
setDbPurgeBusy(false)
|
||||
}
|
||||
}, [backendUrl, systemDbAvailable])
|
||||
|
||||
|
||||
const renderContent = () => {
|
||||
const ra = DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS
|
||||
@@ -435,7 +419,7 @@ export default function SettingsPage() {
|
||||
|
||||
<OpsPanel
|
||||
title="База данных приложения"
|
||||
description="Резервная копия PostgreSQL (pg_dump custom) и сброс таблиц NetFlow. На время операции планировщик и коллектор IPFIX приостанавливаются. Preview: https://reui.io/preview/base/settings-16"
|
||||
description="Резервная копия и восстановление базы приложения. Для сброса потоков откройте настройки NetFlow."
|
||||
contentClassName="divide-y px-5"
|
||||
>
|
||||
{!systemDbAvailable && (
|
||||
@@ -452,7 +436,7 @@ export default function SettingsPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
|
||||
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy}
|
||||
onClick={() => { void handleSystemDatabaseBackup() }}
|
||||
>
|
||||
{dbBackupBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <DownloadIcon className="size-4" />}
|
||||
@@ -468,7 +452,7 @@ export default function SettingsPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
|
||||
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy}
|
||||
onClick={() => setDbRestoreDialogOpen(true)}
|
||||
>
|
||||
<UploadIcon className="size-4" />
|
||||
@@ -481,18 +465,10 @@ export default function SettingsPage() {
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="Сбросить данные NetFlow"
|
||||
description="Удалит сессии и агрегаты NetFlow в PostgreSQL. Ключи WG и пиры не трогает"
|
||||
description="Проверка состояния и подтверждение сброса — в разделе сбора данных"
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={!systemDbAvailable || dbBackupBusy || dbRestoreBusy || dbPurgeBusy}
|
||||
onClick={() => setDbPurgeOpen(true)}
|
||||
>
|
||||
{dbPurgeBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <TrashIcon className="size-4" />}
|
||||
{dbPurgeBusy ? "Сброс…" : "Сбросить"}
|
||||
</Button>
|
||||
{systemDbAvailable ? <Link href="/data-collection#netflow" className={buttonVariants({variant:"outline",size:"sm"})}>Открыть NetFlow</Link> : <Button size="sm" variant="outline" disabled>Открыть NetFlow</Button>}
|
||||
|
||||
</SettingRow>
|
||||
</OpsPanel>
|
||||
|
||||
@@ -926,13 +902,6 @@ export default function SettingsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<NetflowPurgeConfirm
|
||||
open={dbPurgeOpen}
|
||||
busy={dbPurgeBusy}
|
||||
onConfirm={() => { void handleNetflowPurgeConfirm() }}
|
||||
onCancel={() => { if (!dbPurgeBusy) setDbPurgeOpen(false) }}
|
||||
/>
|
||||
|
||||
<FileImportDialog
|
||||
open={dbRestoreDialogOpen}
|
||||
onOpenChange={setDbRestoreDialogOpen}
|
||||
|
||||
@@ -11,10 +11,10 @@ assert.equal(permissionForRequest("GET","/api/traffic/flow/host-files?download=t
|
||||
assert.equal(permissionForRequest("GET","/api/traffic/flow/settings"),"mm:traffic:read")
|
||||
const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth)
|
||||
let invoked=0
|
||||
for(const [method,url] of [["GET","/api/traffic/flow/settings"],["GET","/api/traffic/flow/host-files"],["POST","/api/traffic/flow/settings/generate-keys"],["PUT","/api/traffic/flow/settings"]] as const) app.route({method,url,handler:async()=>{invoked++;return {ok:true}}})
|
||||
for(const [method,url] of [["GET","/api/traffic/flow/settings"],["GET","/api/traffic/flow/host-files"],["POST","/api/traffic/flow/settings/generate-keys"],["PUT","/api/traffic/flow/settings"],["POST","/api/traffic/flow/purge"]] as const) app.route({method,url,handler:async()=>{invoked++;return {ok:true}}})
|
||||
for(const permission of ["mm:dashboard:read","mm:traffic:read","mm:traffic:write","mm:traffic:admin","mm:settings:admin"]){
|
||||
const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("netflow-test-secret"))
|
||||
for(const [method,url] of [["GET","/api/traffic/flow/settings"],["GET","/api/traffic/flow/host-files"],["POST","/api/traffic/flow/settings/generate-keys"],["PUT","/api/traffic/flow/settings"]] as const){
|
||||
for(const [method,url] of [["GET","/api/traffic/flow/settings"],["GET","/api/traffic/flow/host-files"],["POST","/api/traffic/flow/settings/generate-keys"],["PUT","/api/traffic/flow/settings"],["POST","/api/traffic/flow/purge"]] as const){
|
||||
const before=invoked;const response=await app.inject({method,url,headers:{authorization:"Bearer "+token}})
|
||||
const allowed=permission==="mm:traffic:write"||permission==="mm:traffic:admin"||(permission==="mm:traffic:read"&&url.endsWith("/settings")&&method==="GET")
|
||||
assert.equal(response.statusCode,allowed?200:403,permission+" "+method+" "+url);assert.equal(invoked,before+(allowed?1:0))
|
||||
|
||||
@@ -120,6 +120,7 @@ function NetflowSettingsContent({
|
||||
|
||||
return (
|
||||
<>
|
||||
<section id="netflow" aria-label="Настройки NetFlow">
|
||||
<OpsPanel
|
||||
title="Traffic Flow / NetFlow (IPFIX)"
|
||||
description="Дополнение к сбору счётчиков REST. Приём только через WireGuard на хосте Docker MM."
|
||||
@@ -241,6 +242,7 @@ function NetflowSettingsContent({
|
||||
</Button>
|
||||
</div>
|
||||
</OpsPanel>
|
||||
</section>
|
||||
|
||||
<GeoipSettingsSection backendUrl={backendUrl} />
|
||||
|
||||
|
||||
@@ -268,3 +268,5 @@ REST запрос → ожидание → подтверждённый output /
|
||||
Завершён 11Б.3.4.4.7.2: общий NetFlow IP/CIDR/DNS/ports patch, draft и backend проверяют его; инструкция использует фактический порт. 14 E2E, unit/HTTP/JWT, lint и оба builds прошли. Далее 11Б.3.4.4.7.3 — согласованный host export/маска, затем приёмка NetFlow и Evolution.
|
||||
|
||||
Завершён 11Б.3.4.4.7.3: host export из одного снимка, фактические mask/WG/IPFIX ports, strict keys/peers и безопасные однострочные комментарии. Builder/key/HTTP/JWT/build/lint прошли без запуска host/RouterOS. Далее 11Б.3.4.4.8 — приёмка NetFlow; затем 11Б.3.5 Evolution. Нестандартные prefix allocator не приняты.
|
||||
|
||||
Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма.
|
||||
@@ -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.7.3; scheduler и GeoIP приняты локально; NetFlow API/backend/форма/source/adaptive проверены, далее общий сетевой ввод и Evolution |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.4.4.8; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
|
||||
@@ -36,6 +36,15 @@
|
||||
|
||||
|
||||
|
||||
## Блок 11Б.3.4.4.8 — локальная приёмка NetFlow (2026-10-03)
|
||||
|
||||
- Итоговые 19 основных NetFlow E2E (16 form + 3 responsive) и 10 backend source E2E прошли после общего контракта; добавлены дробный порог без default и GET после подтверждённого PUT. JWT gate дополнен purge: traffic reader не вызывает destructive handler. Backend профили/build и frontend production build приняты в предыдущих блоках; последнее изменение navigation/typecheck проверено отдельно.
|
||||
- Выявлен второй purge consumer в Settings: его старый toast игнорировал partial warnings. Заменён переходом /data-collection#netflow к единой панели состояния/прав/подтверждения. Link E2E проверил переход без POST; полный typecheck и lint изменённых компонентов/тестов прошли. Preview ссылку из пользовательского описания базы убрали.
|
||||
- Settings page имеет исходные 2 synchronous-effect lint errors и warnings (backend URL draft, Evolution hydration/dependencies, неиспользованный apiFetch). Это не новая проблема перехода; исправить в следующем settings блоке. Остальные демонстрационные формы, restore/database, Evolution не приняты этим блоком.
|
||||
- 6 снимков обновлены тестами; верх/действия 320 и ПК проверены ранее, заголовок IPFIX теперь общий. ReUI Frame/Form/Alert/Sheet и theme сохранены, vendor не менялся.
|
||||
- NetFlow принят в границах локальных DI/HTTP/UI fixtures. Реальные PG транзакции/lock/worker, WG/Linux, RouterOS/Portal не приняты. Unknown guard не durable между reload/navigation; table lock performance и multi-instance ingest отдельно; текущий IPv4 allocator для нестандартных prefix не принят. Автоматические повторные destructive POST не добавлены.
|
||||
- Далее 11Б.3.5.1 — строгие Evolution settings/save/test API с проверкой сохранённых полей, затем provider known/error/time/context и сохранённая форма. После Evolution — общие Settings/Alerts/Releases/probes schedule по карте этапа.
|
||||
|
||||
## Блок 11Б.3.4.4.7.3 — согласованный host export NetFlow (2026-10-03)
|
||||
|
||||
- Host export читает одну settings row вместе с peersJson, валидирует один снимок и синхронно генерирует Linux/WG/compose файлы. Нет отдельных запросов между файлами и конфигом внутри script.
|
||||
|
||||
@@ -196,3 +196,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
|
||||
Завершён 11Б.3.4.4.7.2: общий NetFlow IP/CIDR/DNS/ports patch, draft и backend проверяют его; инструкция использует фактический порт. 14 E2E, unit/HTTP/JWT, lint и оба builds прошли. Далее 11Б.3.4.4.7.3 — согласованный host export/маска, затем приёмка NetFlow и Evolution.
|
||||
|
||||
Завершён 11Б.3.4.4.7.3: host export из одного снимка, фактические mask/WG/IPFIX ports, strict keys/peers и безопасные однострочные комментарии. Builder/key/HTTP/JWT/build/lint прошли без запуска host/RouterOS. Далее 11Б.3.4.4.8 — приёмка NetFlow; затем 11Б.3.5 Evolution. Нестандартные prefix allocator не приняты.
|
||||
|
||||
Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма.
|
||||
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
@@ -57,3 +57,9 @@ test("NetFlow: сетевой ввод проверяется до HTTP и port
|
||||
await collector.fill("10.255.254.1");const endpoint=page.getByRole("textbox",{name:"Публичный endpoint хоста MM",exact:true});await endpoint.fill("https://host.test");await save(page).click();await expect(page.getByText("Endpoint: укажите IP или DNS-имя без порта и протокола",{exact:true})).toBeVisible();expect(writes).toEqual([])
|
||||
await endpoint.fill("Collector.EXAMPLE.test.");await field(page).fill("4740");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();expect(writes).toEqual([{collectorIp:"10.255.254.1",flowListenPort:4740,publicEndpoint:"Collector.EXAMPLE.test."}]);await expect(page.getByText(/Firewall: разрешите UDP listen WireGuard. UDP 4740/)).toBeVisible();await expect(page.getByText(/ss -ulnp.*4740/)).toBeVisible()
|
||||
})
|
||||
|
||||
test("NetFlow: порог включается с явной долей, без default 5",async({page})=>{await setup(page);const writes:object[]=[];await page.route("**/api/traffic/flow/settings",r=>{if(r.request().method()!=="PUT")return r.fallback();const patch=r.request().postDataJSON();writes.push(patch);return r.fulfill({json:{ok:true,settings:{...settings,...patch}}})});await open(page);await page.getByRole("switch",{name:"Порог доли на карте",exact:true}).click();const share=page.getByRole("textbox",{name:"Минимум % окна",exact:true});await expect(share).toHaveValue("0");await save(page).click();await expect(page.getByText("Порог карты: укажите долю больше 0 и не более 100%",{exact:true})).toBeVisible();expect(writes).toEqual([]);await share.fill("0.25");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();expect(writes).toEqual([{mapServiceMinSharePct:0.25}]);await expect(share).toHaveValue("0.25")})
|
||||
|
||||
test("NetFlow: поздний GET не отменяет подтверждённый PUT",async({page})=>{await setup(page);let pending=false,started=false,completed=false,release!:()=>void;const gate=new Promise<void>(resolve=>release=resolve);await page.route("**/api/traffic/flow/settings",async r=>{if(r.request().method()==="PUT")return r.fulfill({json:{ok:true,settings:{...settings,...r.request().postDataJSON()}}});if(!pending)return r.fallback();started=true;await gate;try{await r.fulfill({json:settings})}catch{}finally{completed=true}});try{await open(page);pending=true;await refresh(page).click();await expect.poll(()=>started).toBe(true);await field(page).fill("4740");await save(page).click();await expect(page.getByText("Сохранение настроек NetFlow подтверждено",{exact:true})).toBeVisible();release();await expect.poll(()=>completed).toBe(true);await expect(field(page)).toHaveValue("4740");await expect(save(page)).toBeDisabled()}finally{release()}})
|
||||
|
||||
test("NetFlow: Settings ведут к единому подтверждению сброса без POST перехода",async({page})=>{await setup(page);let posts=0;page.on("request",r=>{if(new URL(r.url()).pathname==="/api/traffic/flow/purge")posts++});await page.goto("/settings");const link=page.getByRole("link",{name:"Открыть NetFlow",exact:true});await expect(link).toHaveAttribute("href","/data-collection#netflow");await link.click();await expect(page).toHaveURL(/data-collection#netflow$/);await expect(page.getByRole("region",{name:"Настройки NetFlow",exact:true})).toBeVisible();await expect(field(page)).toHaveValue("4739");expect(posts).toBe(0);await expect(page.getByRole("alertdialog")).toHaveCount(0)})
|
||||