diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx index 3b12009b..9fc314f5 100644 --- a/app/(main)/dashboard/page.tsx +++ b/app/(main)/dashboard/page.tsx @@ -105,7 +105,7 @@ export default function DashboardPage() { id: "overlay", label: "Туннели работают", value: dash.availability.overlay ? String(dash.overlay.up) : "—", - hint: !dash.availability.overlay ? "не все источники доступны" : dash.overlay.total > 0 ? `Не работают: ${dash.overlay.down}` : "нет туннелей", + hint: !dash.availability.overlay ? "не все источники доступны" : !dash.reliable.overlay ? "Последний успешный снимок; не все источники обновлены" : dash.overlay.total > 0 ? `Не работают: ${dash.overlay.down}` : "нет туннелей", href: "/gre", icon: , iconClassName: "text-info", diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index c3d1079b..5ea522fb 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-03 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. Следующий блок12.2.6: strict GRE/WireGuard/VXLAN и device failures; затем Internet path, производительность и сквозная регрессия. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. Следующий блок12.2.7: strict Internet path snapshot/fallback и ошибки backend collector, производительность и сквозная регрессия. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index dab058ea..c093d7ce 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,12 @@ +## 12.2.6 — полные каталоги GRE/WireGuard/VXLAN (2026-10-03) + +Dashboard читает существующие строгие schemas GRE/VXLAN/WireGuard, требует явный результат failures (реальный backend передаёт его всегда для общего каталога), проверяет уникальность устройств/интерфейсов. Device failures/malformed/missing/duplicate не становятся успешным пустым каталогом: последний снимок соответствующего kind и время остаются, остальные источники обновляются независимо. Unknown status не переименовывается в down; допустимый VXLAN status передаётся напрямую. KPI указывает «последний успешный снимок» при неполном обновлении; очередь не утверждает исправность. Старый сервер с legacy каталогом без failures требует обновления API и получает честную ошибку, а не ложное отсутствие туннелей. + +Проверки:49 dashboard/permissions browser (9 новых invalid/partial/cache/recovery cases),3 overlay unit, typecheck/ESLint прошли. Unused callback dependency удалена; installed ReUI/Frame/KPI unchanged, checklist применён. Fixtures содержат полные DTO, включая failures. Далее12.2.7: strict Internet path snapshot/fallback; отдельно устранить swallowed ошибки backend collector до сводной приёмки12.2. + ## 12.2.5 — реальные ping наблюдения и разрывы RTT (2026-10-03) /uptime/overview совместимо добавляет observations.samples и общие rangeStartAt/rangeEndAt. Сохраняет исходные числовые поля legacy клиентов; metadata содержит реальные отсортированные timestamp/RTT/loss/status без искусственных samples и ограничена концом снимка (поступление нового sample во время чтения не нарушает окно). Дашборд предпочитает наблюдения: legacy/empty становятся unknown/null, измеренный0 остаётся0, реальный down/100% остаётся ошибкой. Malformed/duplicate/unordered/out-of-window samples сохраняют cache. Source freshness показывает фактическое время, неизвестные проверки исключают утверждение «без ошибок» и дают warning/— в KPI; отсутствующий каталог источника не рисует offline. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index ea4dbe4a..2d29fef8 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -74,3 +74,9 @@ RTT агрегируется в60 настоящих временных корз | Неизвестное состояние в сводке | [Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Snapshot → known/unknown → KPI/очередь → объект | Далее12.2.6: strict GRE/WireGuard/VXLAN и device failures на dashboard; затем Internet path и сводная приёмка12.2. Внешние ограничения сохраняются. + +## 12.2.6 — полные каталоги GRE/WireGuard/VXLAN (2026-10-03) + +Dashboard читает существующие строгие schemas GRE/VXLAN/WireGuard, требует явный результат failures (реальный backend передаёт его всегда для общего каталога), проверяет уникальность устройств/интерфейсов. Device failures/malformed/missing/duplicate не становятся успешным пустым каталогом: последний снимок соответствующего kind и время остаются, остальные источники обновляются независимо. Unknown status не переименовывается в down; допустимый VXLAN status передаётся напрямую. KPI указывает «последний успешный снимок» при неполном обновлении; очередь не утверждает исправность. Старый сервер с legacy каталогом без failures требует обновления API и получает честную ошибку, а не ложное отсутствие туннелей. + +Проверки:49 dashboard/permissions browser (9 новых invalid/partial/cache/recovery cases),3 overlay unit, typecheck/ESLint прошли. Unused callback dependency удалена; installed ReUI/Frame/KPI unchanged, checklist применён. Fixtures содержат полные DTO, включая failures. Далее12.2.7: strict Internet path snapshot/fallback; отдельно устранить swallowed ошибки backend collector до сводной приёмки12.2. diff --git a/hooks/use-dashboard-live.ts b/hooks/use-dashboard-live.ts index 779e3293..9be20352 100644 --- a/hooks/use-dashboard-live.ts +++ b/hooks/use-dashboard-live.ts @@ -15,7 +15,6 @@ import { import { useDataSource } from "@/lib/data-source" import { requestJson } from "@/shared/api/http-client" import { listEvents } from "@/shared/api/events" -import { listWireGuard } from "@/shared/api/wireguard" import type { EventItem } from "@mmapp/contracts/events" import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency" import { @@ -29,6 +28,7 @@ import type { FiltersRulesetRow, RouteOptimizerSpeedProbe } from "@/lib/route-op import { updateDashboardSource, type DashboardSource, type DashboardSources } from "@/lib/dashboard-source-state" import {can,ensureAuthConfig} from "@/lib/auth" +import {parseDashboardGre,parseDashboardWireGuard,parseDashboardVxlan,type DashboardGreTunnel} from "@/lib/dashboard-overlays" import type {DashboardProbe} from "@/lib/dashboard-probes" import {parseDashboardServers,parseDashboardProbes,type DashboardServerRow} from "@/lib/dashboard-readers" import {parseDashboardTraffic,type DashboardTraffic} from "@/lib/dashboard-traffic" @@ -63,26 +63,7 @@ function readMockDashboardStarIds(): Set { type BackendServerRow=DashboardServerRow -interface ApiGreTunnelRow { - id: string - name: string - serverId: string - localAddress: string - remoteAddress: string - localInnerIp: string - remoteInnerIp: string - poolId: string - ipsec: null - mtu: number - keepaliveInterval: number - keepaliveRetries: number - dscp: "inherit" | number - clampTcpMss: boolean - allowFastPath: boolean - comment: string - enabled: boolean - status: "up" | "down" | "degraded" -} +type ApiGreTunnelRow=DashboardGreTunnel interface InternetPathSnapshotPayload { sampledAt: string @@ -248,10 +229,10 @@ export function useDashboardLive() { apiFetch<{ rulesets: Array<{ rules?: unknown[] }> }>("/api/filters/rules"), apiFetch("/api/bgp/catalog").then(parseDashboardBgp), apiFetch<{ snapshot: InternetPathSnapshotPayload | null }>("/api/internet-path/latest"), - apiFetch<{ tunnels?: ApiGreTunnelRow[] }>("/api/filters/gre-tunnels"), - dashboardRequest("/api/wireguard",()=>listWireGuard(backendUrl)), + apiFetch("/api/filters/gre-tunnels").then(parseDashboardGre), + apiFetch("/api/wireguard").then(parseDashboardWireGuard), apiFetch("/api/traffic/servers?range=1h").then(parseDashboardTraffic), - apiFetch<{ tunnels?: Array<{ id: string; serverId: string; name: string; status: OverlayItem["status"] }> }>("/api/vxlan"), + apiFetch("/api/vxlan").then(parseDashboardVxlan), ]) if (!current()) return @@ -302,7 +283,7 @@ export function useDashboardLive() { name: t.name, kind: "vxlan" as const, href: "/vxlan?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id), - status: t.status === "up" ? "up" : "down", + status: t.status, })) : [] setOverlayItems(previous => [ @@ -396,7 +377,7 @@ export function useDashboardLive() { setInternetPathLoading(false) } } - }, [apiFetch, backendUrl, isLive, recordSource]) + }, [apiFetch, isLive, recordSource]) const fetchRecentEvents = useCallback(async (_silent: boolean) => { if (!isLive) { diff --git a/lib/dashboard-overlays.test.ts b/lib/dashboard-overlays.test.ts new file mode 100644 index 00000000..5d8944f8 --- /dev/null +++ b/lib/dashboard-overlays.test.ts @@ -0,0 +1,8 @@ +import assert from "node:assert/strict" +import {test} from "node:test" +import {parseDashboardGre,parseDashboardWireGuard,parseDashboardVxlan} from "./dashboard-overlays" +import {dashboardGre,dashboardWg,dashboardVxlan} from "../tests/e2e/fixtures/dashboard-overlays" +for(const [kind,parse,key,row] of [["GRE",parseDashboardGre,"tunnels",dashboardGre],["VXLAN",parseDashboardVxlan,"tunnels",dashboardVxlan],["WG",parseDashboardWireGuard,"interfaces",dashboardWg]] as const)test("strict complete dashboard overlay catalog/"+kind,()=>{ + assert.ok(parse({[key]:[row],failures:[]}));assert.ok(parse({[key]:[],failures:[]})) + for(const raw of [{},{[key]:[]},{[key]:[{}],failures:[]},{[key]:[row,row],failures:[]},{[key]:[{...row,status:"unknown"}],failures:[]},{[key]:[row],failures:[{serverId:"2",error:"timeout"}]}])assert.throws(()=>parse(raw)) +}) diff --git a/lib/dashboard-overlays.ts b/lib/dashboard-overlays.ts new file mode 100644 index 00000000..02b4dabb --- /dev/null +++ b/lib/dashboard-overlays.ts @@ -0,0 +1,13 @@ +import {greResponseSchema} from "@/lib/gre-response" +import {vxlanResponseSchema} from "@/lib/vxlan-response" +import {wgListResponseSchema} from "@mmapp/contracts/wireguard" +import type {z} from "zod" +function complete(raw:unknown,failures:Array<{serverId:string;serverName?:string;error:string}>,kind:string){ + if(!raw||typeof raw!=="object"||!("failures" in raw)||!Array.isArray(raw.failures))throw Error("Результат опроса устройств "+kind+" не подтверждён") + if(new Set(failures.map(f=>f.serverId)).size!==failures.length||failures.some(f=>!f.serverId.trim()))throw Error("Некорректные ошибки каталога "+kind) + if(failures.length)throw Error("Не опрошены устройства "+kind+": "+failures.map(f=>(f.serverName??f.serverId)+": "+f.error).join("; ")) +} +export type DashboardGreTunnel=z.infer["tunnels"][number] +export function parseDashboardGre(raw:unknown){const p=greResponseSchema.safeParse(raw);if(!p.success)throw Error("Некорректный каталог GRE");complete(raw,p.data.failures,"GRE");return p.data} +export function parseDashboardVxlan(raw:unknown){const p=vxlanResponseSchema.safeParse(raw);if(!p.success)throw Error("Некорректный каталог VXLAN");complete(raw,p.data.failures,"VXLAN");return p.data} +export function parseDashboardWireGuard(raw:unknown){const p=wgListResponseSchema.safeParse(raw);if(!p.success)throw Error("Некорректный каталог WireGuard");complete(raw,p.data.failures??[],"WireGuard");if(new Set(p.data.interfaces.map(i=>JSON.stringify([i.serverId,i.id]))).size!==p.data.interfaces.length||new Set(p.data.interfaces.map(i=>JSON.stringify([i.serverId,i.rosId]))).size!==p.data.interfaces.length)throw Error("Повторяющийся интерфейс WireGuard");return p.data} diff --git a/tests/e2e/dashboard-data.spec.ts b/tests/e2e/dashboard-data.spec.ts index fabd09b5..1fab60a7 100644 --- a/tests/e2e/dashboard-data.spec.ts +++ b/tests/e2e/dashboard-data.spec.ts @@ -1,10 +1,11 @@ import { test, expect, type Page } from "@playwright/test" import { serverReadSchema } from "../../packages/contracts/src/servers" +import {dashboardGre,dashboardVxlan,dashboardWg} from "./fixtures/dashboard-overlays" import {bgpSession} from "./fixtures/bgp" async function setup(page: Page, initiallyFailed = false, serverCount = 1, initialBgpPayload?:unknown) { - const control = { failed: initiallyFailed, partial: false, prefixes: 321 as number|null, serversPayload:undefined as unknown,probesPayload:undefined as unknown, trafficPayload: undefined as unknown, bgpPayload: initialBgpPayload as unknown, hold: false, holdPath: "/api/bgp/catalog", fallback: false, failFallback: false, pending: 0, release: () => {} } + const control = { failed: initiallyFailed, partial: false, prefixes: 321 as number|null, grePayload:undefined as unknown,wgPayload:undefined as unknown,vxlanPayload:undefined as unknown,serversPayload:undefined as unknown,probesPayload:undefined as unknown, trafficPayload: undefined as unknown, bgpPayload: initialBgpPayload as unknown, hold: false, holdPath: "/api/bgp/catalog", fallback: false, failFallback: false, pending: 0, release: () => {} } let release!: () => void const gate = new Promise(resolve => { release = resolve }) control.release = release @@ -30,9 +31,9 @@ async function setup(page: Page, initiallyFailed = false, serverCount = 1, initi : path === "/api/uptime/overview" ? control.probesPayload !== undefined ? control.probesPayload : { probes: [] } : path === "/api/bgp/catalog" ? control.bgpPayload !== undefined ? control.bgpPayload : {sessions:[bgpSession(1,prefixes)],failures:[]} : path === "/api/filters/rules" ? { rulesets: [] } - : path === "/api/filters/gre-tunnels" ? { tunnels: [{ id: "g1", name: "GRE сохранён", status: "down", serverId: "1" }] } - : path === "/api/wireguard" ? { interfaces: [], peers: [] } - : path === "/api/vxlan" ? { tunnels: [] } + : path === "/api/filters/gre-tunnels" ? control.grePayload!==undefined?control.grePayload:{ tunnels: [dashboardGre],failures:[] } + : path === "/api/wireguard" ? control.wgPayload!==undefined?control.wgPayload:{ interfaces: [],failures:[] } + : path === "/api/vxlan" ? control.vxlanPayload!==undefined?control.vxlanPayload:{ tunnels: [],failures:[] } : path === "/api/traffic/servers" ? control.trafficPayload !== undefined ? control.trafficPayload : { servers: [{ id: "1", rxNow: 1000, txNow: 500, rxSeries: [1000], txSeries: [500],observations:{rxNow:true,txNow:true,rxSeries:[true],txSeries:[true],sampledAt:"2026-09-20T00:00:00Z"} }] } : path === "/api/internet-path/latest" ? { snapshot: control.fallback ? null : snapshot } : path === "/api/uptime/speed-probes" ? { probes: [] } @@ -241,3 +242,7 @@ for(const state of ["legacy","unmeasured","zero","failure","gaps","invalid"])tes const control=await setup(page);await expect(refresh(page)).toBeEnabled();const probe={id:"ping-observation",srcServerId:"1",name:"Измерение ping",target:"192.0.2.2",filter:"main",rtt:0,loss:0,status:"up",series:[0],enabled:true,showOnDashboard:true};const sample=(minute:number,rtt:number|null,status="up",loss=0)=>({sampledAt:"2026-10-03T00:"+String(minute).padStart(2,"0")+":00Z",rtt,status,loss});const samples=state==="unmeasured"?[]:state==="failure"?[sample(30,null,"down",100)]:state==="gaps"?[sample(29,5),sample(30,6),sample(32,0)]:state==="invalid"?[sample(30,0),sample(30,0)]:[sample(30,0)];control.probesPayload=state==="legacy"?{probes:[probe]}:{rangeStartAt:"2026-10-03T00:00:00Z",rangeEndAt:"2026-10-03T01:00:00Z",probes:[{...probe,observations:{samples}}]};await refresh(page).click();await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="probes"]');if(state==="invalid"){await expect(source).toContainText("ошибка обновления");return}await expect(source).not.toContainText("ошибка");const row=page.locator('tr[data-row-id]').filter({hasText:"Измерение ping"});await expect(row).toHaveCount(1); if(state==="legacy"||state==="unmeasured"){await expect(row).toContainText("Неизвестно");await expect(page.getByRole("link").filter({hasText:"Проверки с проблемами"})).toContainText("неизвестны: 1");await expect(page.getByText("Проверки без ошибок",{exact:true})).toHaveCount(0);await expect(row).not.toContainText("100%");await expect(page.locator('[data-latency-source]')).toHaveCount(0)}else if(state==="failure"){await expect(row).toContainText("Оффлайн");await expect(row).toContainText("100%");await expect(page.getByRole("link").filter({hasText:"Проверки с проблемами"})).toContainText("1 ошибок")}else{await expect(row).toContainText("Онлайн");await expect(row).toContainText("0%");await expect(page.locator('[data-latency-isolated]')).toHaveCount(1);if(state==="gaps"){await expect(page.locator('.recharts-line-curve')).toHaveAttribute("d",/M.*M/);for(const width of [1440,320]){await page.setViewportSize({width,height:900});await page.getByRole("region",{name:"История RTT по источникам",exact:true}).scrollIntoViewIfNeeded();expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);await page.screenshot({path:"docs/ux-ui/screens/dashboard-observed-ping-"+width+".png",animations:"disabled"})}}else await expect(page.locator('[data-latency-segment]')).toHaveCount(0);await expect(source.locator("time").last()).toHaveAttribute("datetime",new Date(samples.at(-1)!.sampledAt).toISOString())} }) + +for(const kind of ["gre","wg","vxlan"] as const)for(const broken of ["missing","partial","duplicate"])test("Dashboard overlay complete source and cache/"+kind+"/"+broken,async({page})=>{ + const control=await setup(page);await expect(refresh(page)).toBeEnabled();const source=page.locator('[data-source="'+kind+'"]'),controlKey=kind==="gre"?"grePayload":kind==="wg"?"wgPayload":"vxlanPayload",rowsKey=kind==="wg"?"interfaces":"tunnels",row=kind==="gre"?dashboardGre:kind==="wg"?dashboardWg:dashboardVxlan;control[controlKey]={[rowsKey]:[row],failures:[]};await refresh(page).click();await expect(source).not.toContainText("ошибка");const receivedAt=await source.locator("time").getAttribute("datetime");control[controlKey]=broken==="missing"?{}:broken==="partial"?{[rowsKey]:[row],failures:[{serverId:"2",serverName:"Узел без ответа",error:"timeout"}]}:{[rowsKey]:[row,row],failures:[]};await refresh(page).click();await expect(source).toContainText("ошибка обновления");await expect(source.locator("time")).toHaveAttribute("datetime",receivedAt!);await expect(page.getByRole("link").filter({hasText:"Туннели работают"})).toContainText("Последний успешный снимок");await expect(page.locator('[data-source="servers"]')).not.toContainText("ошибка");if(kind==="gre")await expect(page.getByText("GRE сохранён",{exact:true})).toBeVisible();control[controlKey]={[rowsKey]:[],failures:[]};await refresh(page).click();await expect(source).not.toContainText("ошибка");if(kind==="gre")await expect(page.getByText("GRE сохранён",{exact:true})).toHaveCount(0) +}) diff --git a/tests/e2e/dashboard-permissions.spec.ts b/tests/e2e/dashboard-permissions.spec.ts index 4ec46d24..abda01b5 100644 --- a/tests/e2e/dashboard-permissions.spec.ts +++ b/tests/e2e/dashboard-permissions.spec.ts @@ -3,5 +3,5 @@ import {bgpSession} from "./fixtures/bgp" for(const bgp of [false,true])test("Dashboard limited JWT requests only authorized data sources/"+bgp,async({page})=>{ const permissions=["mm:dashboard:read",...(bgp?["mm:bgp:read"]:[])],token="header."+Buffer.from(JSON.stringify({sub:"fixture",apps:["mm"],permissions,exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture" await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);const calls:string[]=[] - await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required:true}});calls.push(path);return r.fulfill({json:path==="/api/vxlan"?{tunnels:[]}:path==="/api/events"?{events:[]}:path==="/api/bgp/catalog"?{sessions:[bgpSession()],failures:[]}:{}})});await page.goto("/dashboard");const refresh=page.getByRole("button",{name:"Обновить данные",exact:true});await expect(refresh).toBeEnabled();await expect(page.locator('[data-source="servers"]')).toContainText("Нет доступа");await refresh.click();await expect(refresh).toBeEnabled();for(const path of ["/api/servers","/api/uptime/overview","/api/filters/rules","/api/filters/gre-tunnels","/api/internet-path/latest","/api/wireguard","/api/traffic/servers"])expect(calls).not.toContain(path);expect(calls.includes("/api/bgp/catalog")).toBe(bgp);const tile=page.getByRole("link").filter({hasText:"Префиксы BGP"});await expect(tile).toContainText(bgp?"321":"—");expect(calls).toContain("/api/vxlan") + await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required:true}});calls.push(path);return r.fulfill({json:path==="/api/vxlan"?{tunnels:[],failures:[]}:path==="/api/events"?{events:[]}:path==="/api/bgp/catalog"?{sessions:[bgpSession()],failures:[]}:{}})});await page.goto("/dashboard");const refresh=page.getByRole("button",{name:"Обновить данные",exact:true});await expect(refresh).toBeEnabled();await expect(page.locator('[data-source="servers"]')).toContainText("Нет доступа");await refresh.click();await expect(refresh).toBeEnabled();for(const path of ["/api/servers","/api/uptime/overview","/api/filters/rules","/api/filters/gre-tunnels","/api/internet-path/latest","/api/wireguard","/api/traffic/servers"])expect(calls).not.toContain(path);expect(calls.includes("/api/bgp/catalog")).toBe(bgp);const tile=page.getByRole("link").filter({hasText:"Префиксы BGP"});await expect(tile).toContainText(bgp?"321":"—");expect(calls).toContain("/api/vxlan") }) diff --git a/tests/e2e/fixtures/dashboard-overlays.ts b/tests/e2e/fixtures/dashboard-overlays.ts new file mode 100644 index 00000000..ce8c77e3 --- /dev/null +++ b/tests/e2e/fixtures/dashboard-overlays.ts @@ -0,0 +1,4 @@ +import type {DashboardGreTunnel} from "@/lib/dashboard-overlays" +export const dashboardGre:DashboardGreTunnel={id:"g1",name:"GRE сохранён",serverId:"1",status:"down",localAddress:"192.0.2.1",remoteAddress:"192.0.2.2",localInnerIp:"10.0.0.1/30",remoteInnerIp:"10.0.0.2/30",poolId:"live",ipsec:null,mtu:1400,keepaliveInterval:10,keepaliveRetries:3,dscp:"inherit",clampTcpMss:true,allowFastPath:true,comment:"",enabled:true} +export const dashboardVxlan={id:"v1",name:"VXLAN fixture",serverId:"1",vni:1,port:4789,dstPort:4789,vtepIp:"192.0.2.1",remoteVteps:["192.0.2.2"],l2mtu:1400,arpProxy:false,macLearning:true,comment:"",enabled:true,status:"up"} +export const dashboardWg={id:"w1",rosId:"*1",name:"WG fixture",serverId:"1",serverName:"Узел",listenPort:51820,mtu:1420,peers:[],comment:"",enabled:true,status:"up"}