From 059fd935204ff38c07df241e7b4944b9a6f04557 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 18:03:59 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B0=20=D1=81=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/route-optimizer/page.tsx | 5 +- components/optimizer-homes-list.tsx | 45 ++++++++++++++++++ docs/ux-ui-performance.md | 2 + docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 ++++ .../optimizer-homes-pages-2026-10-04.json | 7 +++ .../screens/optimizer-homes-pages-320.png | Bin 0 -> 56378 bytes tests/e2e/large-optimizer-homes.spec.ts | 42 ++++++++++++++-- tests/e2e/route-optimizer-tables.spec.ts | 20 +++++++- 10 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 components/optimizer-homes-list.tsx create mode 100644 docs/ux-ui/acceptance/optimizer-homes-pages-2026-10-04.json create mode 100644 docs/ux-ui/screens/optimizer-homes-pages-320.png diff --git a/app/(main)/route-optimizer/page.tsx b/app/(main)/route-optimizer/page.tsx index fbac66dd..6fbcc1bc 100644 --- a/app/(main)/route-optimizer/page.tsx +++ b/app/(main)/route-optimizer/page.tsx @@ -6,6 +6,7 @@ import {useSearchParams} from "next/navigation" import {can} from "@/lib/auth" import {ospfCatalogSchema,ospfOptimizeSchema} from "@/lib/ospf-response" import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response" +import { OptimizerHomesList } from "@/components/optimizer-homes-list" import { PageHeader } from "@/components/page-header" import { RouteOptimizerWanMatrixDataGrid } from "@/components/data-grids/route-optimizer-wan-matrix-data-grid" import { RouteOptimizerFullRoutesDataGrid } from "@/components/data-grids/route-optimizer-full-routes-data-grid" @@ -1052,7 +1053,7 @@ function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer {/* Per-home-router cards */} - {data?.homes.map((entry) => ( + {data && {entry => ( - ))} + )}} diff --git a/components/optimizer-homes-list.tsx b/components/optimizer-homes-list.tsx new file mode 100644 index 00000000..007782d1 --- /dev/null +++ b/components/optimizer-homes-list.tsx @@ -0,0 +1,45 @@ +"use client" + +import { useEffect, useMemo, type ReactNode } from "react" +import { useSearchParams } from "next/navigation" +import type { HomeEntry } from "@/lib/route-optimizer-data" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +export function OptimizerHomesList({ homes, children }: { homes: HomeEntry[]; children: (entry: HomeEntry) => ReactNode }) { + const params = useSearchParams() + const query = params.get("home_q") ?? "" + const rawPage = params.get("home_page") ?? "1" + const requestedPage = /^[1-9]\d*$/.test(rawPage) && Number.isSafeInteger(Number(rawPage)) ? Number(rawPage) - 1 : 0 + const filtered = useMemo(() => { + const value = query.trim().toLowerCase() + return value ? homes.filter(({ home }) => `${home.label} ${home.id} ${home.ip} ${home.site} ${home.country}`.toLowerCase().includes(value)) : homes + }, [homes, query]) + const pages = Math.max(1, Math.ceil(filtered.length / 5)) + const page = Math.min(requestedPage, pages - 1) + function change(queryValue: string, pageValue: number, push = false) { + const url = new URL(window.location.href) + if (queryValue) url.searchParams.set("home_q", queryValue); else url.searchParams.delete("home_q") + if (pageValue) url.searchParams.set("home_page", String(pageValue + 1)); else url.searchParams.delete("home_page") + window.history[push ? "pushState" : "replaceState"](null, "", url.pathname + url.search + url.hash) + } + useEffect(() => { + if (requestedPage < pages) return + const url = new URL(window.location.href) + if (page) url.searchParams.set("home_page", String(page + 1)); else url.searchParams.delete("home_page") + window.history.replaceState(null, "", url.pathname + url.search + url.hash) + }, [requestedPage, pages, page]) + const controlled = homes.length > 5 || !!query || requestedPage > 0 + return
+ {controlled &&
+ change(event.target.value, 0)} /> +

Показаны {filtered.length ? page * 5 + 1 : 0}–{Math.min((page + 1) * 5, filtered.length)} из {filtered.length} устройств. Всего {homes.length}.

+
+ + +
+ {!filtered.length &&

Домашние устройства по запросу не найдены.

} +
} + {filtered.slice(page * 5, (page + 1) * 5).map(children)} +
+} diff --git a/docs/ux-ui-performance.md b/docs/ux-ui-performance.md index f2866ff5..89237bf3 100644 --- a/docs/ux-ui-performance.md +++ b/docs/ux-ui-performance.md @@ -103,3 +103,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич 12.3.53: OSPF preview1000 interfaces/245293 bytes →50 rows/924 region nodes/1610 page DOM.47+16 main/8 context/type/lint/build. Полный POST/receipt1000 при search1, sort/cache/clamp/320px приняты.8 manualPagination:true; далее many HomeRouterCard/матрицы. 12.3.54:1000 HomeRouterCard/179347 bytes →40668 page DOM/11496ms dev ready.1 browser/test lint, baseline без WAN не означает принятую производительность. Далее bounded cards12.3.55. + +12.3.55:1000 HomeRouterCard →5/1870 page DOM/1606ms dev ready, full search/URL/refresh/cache/clamp/320px/axe/pin preserved.23+8 main/8 context/type/lint/build. Полный CPU расчёт и1000 native device options остаются; далее1000 full routes. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 0ae08db8..d7575f5b 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-04: 12.3.54 сохранён — baseline1000 HomeRouterCard/40668 DOM/11496ms dev ready;1 browser/test lint. Это не приёмка. Далее12.3.55 — ограничить карточки с полным поиском/URL/refresh/clamp; затем матрицы/общая регрессия. Внешняя12.5 отдельно. +Актуальное продолжение 2026-10-04: 12.3.55 принят локально:1000 HomeRouterCard→5/1870 page DOM, полный поиск/URL/reload/back/refresh/cache/clamp/320px/axe; закрепление Community сохраняется.23+8 main/8 context/type/lint/build. Далее12.3.56 — исходный профиль полной таблицы маршрутов; затем её ограничение/матрицы/общая регрессия. Внешняя12.5 отдельно. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 9cb83bea..8180c90d 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-04: 12.3.54 сохранён — baseline1000 HomeRouterCard/40668 DOM/11496ms dev ready;1 browser/test lint. Это не приёмка. Далее12.3.55 — ограничить карточки с полным поиском/URL/refresh/clamp; затем матрицы/общая регрессия. Внешняя12.5 отдельно. +Актуальное продолжение 2026-10-04: 12.3.55 принят локально:1000 HomeRouterCard→5/1870 page DOM, полный поиск/URL/reload/back/refresh/cache/clamp/320px/axe; закрепление Community сохраняется.23+8 main/8 context/type/lint/build. Далее12.3.56 — исходный профиль полной таблицы маршрутов; затем её ограничение/матрицы/общая регрессия. Внешняя12.5 отдельно. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index bb3d4314..c9cdf7c8 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -637,3 +637,12 @@ TopologyGraph группирует видимые edges по неориенти Полный optimizerServersSchema1000 home-router/179347 bytes, без подтверждённых WAN/probes/filters.1/1 browser и ESLint теста прошли.1000 HomeRouterCard/40668 page DOM/11496ms dev ready; document320px не шире viewport. [Профиль](ux-ui/acceptance/optimizer-homes-baseline-2026-10-04.json), [исходный320px](ux-ui/screens/optimizer-homes-baseline-320.png) сохранены. Это baseline, не приёмка производительности; dev latency не production benchmark. Нет WAN — это валидная live ветка, не подмена неизвестных данных нулями. Продуктовый код не менялся, build повторно не нужен. Следующий12.3.55: ограниченный DOM HomeRouterCard/поиск полного каталога/URL/refresh/clamp/320px. Расчёт полного массива остаётся отдельным CPU риском; раскрытые full-routes и WAN dynamic columns требуют других профилей. Общая регрессия и внешняя12.5 отдельно. Локальный коммит без push. + + +## 12.3.55 — ограниченные карточки домашних устройств (2026-10-04) + +OptimizerHomesList показывает5 карточек на страницу, поиск по полным label/id/IP/site/country. home_q/home_page в URL сохраняют server/keep/hash; поиск replaceState/reset page, pagination pushState, clamp после нового полного снимка нормализует URL. Малый каталог до5 без фильтра сохраняет прежнюю композицию. Расчёт и полный DTO не обрезаются; глобальные pinned/applied Sets остаются у родителя. Карточка при уходе со страницы перемонтируется, её локальная вкладка сбрасывается — восстановление вкладки отдельно не принято. + +23 профильных main прошли; единственный сбой нового pin-сценария был неверным selector теста (row key home::community::index); исправлен, финальный suite8/8 passed.8/8 backend/mode context, TypeScript без incremental/полный ESLint страницы/компонента/тестов/production build прошли.1000 home-router/179347 bytes →5 cards/1870 page DOM/1606ms dev ready вместо40668/11496ms. Проверены page200/refresh, search1000/reload/back/keep/server, cache503/full snapshot, shrink1000→1/URL clamp, pin Community→page2→page1,320px/HTML pagination44px/axe. [Профиль](ux-ui/acceptance/optimizer-homes-pages-2026-10-04.json), [320px](ux-ui/screens/optimizer-homes-pages-320.png) сохранены и просмотрены. Dev latency не production benchmark; native OSPF device select всё ещё содержит1000 options. + +Следующий12.3.56: полный профиль WAN10×JH10×Exit10/1000 routes и раскрытой таблицы. WAN matrix columns/полный расчёт combinatorics и24 commRecs остаются самостоятельными ограничениями.8 specialized manualPagination:true остаются. Общая регрессия после delta и внешняя12.5 отдельно. Локальный коммит без push. diff --git a/docs/ux-ui/acceptance/optimizer-homes-pages-2026-10-04.json b/docs/ux-ui/acceptance/optimizer-homes-pages-2026-10-04.json new file mode 100644 index 00000000..84f14a5d --- /dev/null +++ b/docs/ux-ui/acceptance/optimizer-homes-pages-2026-10-04.json @@ -0,0 +1,7 @@ +{ + "servers": 1000, + "bytes": 179347, + "homeCards": 5, + "pageNodes": 1870, + "readyMs": 1606 +} \ No newline at end of file diff --git a/docs/ux-ui/screens/optimizer-homes-pages-320.png b/docs/ux-ui/screens/optimizer-homes-pages-320.png new file mode 100644 index 0000000000000000000000000000000000000000..2644a81730b0b31fff12abe662171fd58e1a9835 GIT binary patch literal 56378 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 z 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 z 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 aL+GAdzg&xLs=n`mkQP@EtNdyd^j`qwPo8A} literal 0 HcmV?d00001 diff --git a/tests/e2e/large-optimizer-homes.spec.ts b/tests/e2e/large-optimizer-homes.spec.ts index 5bc02fbf..31669c01 100644 --- a/tests/e2e/large-optimizer-homes.spec.ts +++ b/tests/e2e/large-optimizer-homes.spec.ts @@ -1,22 +1,54 @@ import { test, expect } from "@playwright/test" import { writeFileSync } from "node:fs" +import axe from "axe-core" import { optimizerServersSchema } from "../../lib/route-optimizer-response" test("Оптимизатор1000 домашних устройств: профиль полного экрана", async ({ page }, info) => { test.setTimeout(90000) - const servers = optimizerServersSchema.parse(Array.from({ length: 1000 }, (_, i) => ({ id: i+1, name: `Домашний роутер ${i+1}`, host: `10.${Math.floor(i/256)}.${i%256}.1`, type: "home-router", country: "RU", site: "MSK", enabled: true, status: null }))) + let servers = optimizerServersSchema.parse(Array.from({ length: 1000 }, (_, i) => ({ id: i+1, name: `Домашний роутер ${i+1}`, host: `10.${Math.floor(i/256)}.${i%256}.1`, type: "home-router", country: "RU", site: "MSK", enabled: true, status: null }))) + let failed = false + const writes: string[] = [] await page.route("**/health", r => r.fulfill({ json: { ok: true } })) await page.route("**/api/**", r => { const path = new URL(r.request().url()).pathname + if (r.request().method() !== "GET" && !path.endsWith("/preview")) writes.push(path) + if (path === "/api/servers" && failed) return r.fulfill({ status: 503, json: { error: "Каталог домашних устройств недоступен" } }) return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes: [] } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/ospf/optimize/preview") ? { serverId: 1, serverName: servers[0].name, pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} }) }) - const start = Date.now(); await page.goto("/route-optimizer?server=1&keep=1") + const start = Date.now(); await page.goto("/route-optimizer?server=1&keep=1&home_page=200") const cards = page.getByRole("button", { name: "WAN × JH (0×0)", exact: true }) - await expect(cards).toHaveCount(1000) + await expect(cards).toHaveCount(5) + const scope = page.getByRole("region", { name: "Домашние устройства оптимизатора" }) + await expect(scope.getByRole("status")).toContainText("996–1000 из 1000") + await expect(scope.getByText("Домашний роутер 996", { exact: true })).toBeVisible() const profile = { servers: servers.length, bytes: Buffer.byteLength(JSON.stringify(servers)), homeCards: await cards.count(), pageNodes: await page.locator("*").count(), readyMs: Date.now()-start } - console.log("Optimizer homes", JSON.stringify(profile)); writeFileSync(info.outputPath("optimizer-homes-profile.json"), JSON.stringify(profile, null, 2)) + console.log("Optimizer homes pages", JSON.stringify(profile)); writeFileSync(info.outputPath("optimizer-homes-profile.json"), JSON.stringify(profile, null, 2)) + await scope.getByRole("button", { name: "Предыдущая страница домашних устройств" }).click() + await expect(scope.getByRole("status")).toContainText("991–995 из 1000") + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(scope.getByRole("status")).toContainText("991–995 из 1000") + const search = scope.getByRole("textbox", { name: "Поиск домашних устройств оптимизатора" }) + await search.fill("Домашний роутер 1000"); await expect(cards).toHaveCount(1) + await expect(scope.getByText("Домашний роутер 1000", { exact: true })).toBeVisible() + await page.reload(); await expect(cards).toHaveCount(1); await expect(search).toHaveValue("Домашний роутер 1000") + await expect(page).toHaveURL(url => url.searchParams.get("keep") === "1" && url.searchParams.get("server") === "1") + await search.fill(""); await expect(cards).toHaveCount(5) + await expect(scope.getByText("Домашний роутер 1", { exact: true })).toBeVisible() + await page.goBack(); await expect(scope.getByRole("status")).toContainText("996–1000 из 1000") + failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(page.getByRole("alert").filter({ hasText: "Последний полный снимок" })).toBeVisible() + await expect(scope.getByRole("status")).toContainText("996–1000 из 1000") await page.addStyleTag({ content: "nextjs-portal{display:none!important}" }) await page.setViewportSize({ width: 320, height: 1000 }) expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(320) - await page.screenshot({ path: info.outputPath("optimizer-homes-baseline-320.png"), animations: "disabled" }) + await scope.evaluate(el => el.scrollIntoView({ block: "start" })) + await page.screenshot({ path: info.outputPath("optimizer-homes-pages-320.png"), animations: "disabled" }) + await page.addScriptTag({ content: axe.source }) + expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Домашние устройства оптимизатора"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([]) + for (const name of ["Предыдущая страница домашних устройств", "Следующая страница домашних устройств"]) expect((await scope.getByRole("button", { name }).boundingBox())!.height).toBeGreaterThanOrEqual(44) + failed = false; servers = servers.slice(0, 1) + await page.getByRole("button", { name: "Обновить", exact: true }).click() + await expect(cards).toHaveCount(1); await expect(scope.getByText("Домашний роутер 1", { exact: true })).toBeVisible() + await expect(page).toHaveURL(url => !url.searchParams.has("home_page") && url.searchParams.get("server") === "1" && url.searchParams.get("keep") === "1") + expect(writes).toEqual([]) }) diff --git a/tests/e2e/route-optimizer-tables.spec.ts b/tests/e2e/route-optimizer-tables.spec.ts index 61c571b1..26a2d93c 100644 --- a/tests/e2e/route-optimizer-tables.spec.ts +++ b/tests/e2e/route-optimizer-tables.spec.ts @@ -8,7 +8,7 @@ const servers: OptimizerApiServer[] = (["home-router", "jump-host", "exit-node"] ...(i === 0 ? { wanUplinks: ids.map(id => ({ id: `wan${id}`, name: `WAN${id}`, isp: "ISP", iface: `ether${id}`, ip: `198.51.100.${id}`, maxDl: 100, maxUl: 100 })) } : {}), })) -async function setup(page: Page, preview: "ok" | "empty" | "error" = "ok", missingExit = false) { +async function setup(page: Page, preview: "ok" | "empty" | "error" = "ok", missingExit = false, manyHomes = false) { await page.route("**/health", route => route.fulfill({ json: { ok: true } })) await page.route("**/api/**", route => { const path = new URL(route.request().url()).pathname @@ -17,7 +17,7 @@ async function setup(page: Page, preview: "ok" | "empty" | "error" = "ok", missi const interfaces = preview === "empty" ? [] : ids.map(i => ({ id: String(i), interface: `ether${i}`, currentCost: i, optimalCost: 20, score: i, pingMs: i, dlMbps: 100, ulMbps: 50 })) return route.fulfill({ json: {serverId:1,serverName:"Роутер 1",pingWeight:60,unchangedCount:0, interfaces, changes: interfaces, changedCount: interfaces.length, interfacesTotal: interfaces.length } }) } - return route.fulfill({ json: path === "/api/servers" ? servers.filter(server => !missingExit || server.type !== "exit-node") + return route.fulfill({ json: path === "/api/servers" ? [...servers.filter(server => !missingExit || server.type !== "exit-node"), ...(manyHomes ? Array.from({ length: 5 }, (_, i) => ({ ...servers[0], id: i+4, name: `Другой дом ${i+4}`, wanUplinks: [] })) : [])] : path === "/api/auth/config" ? { required: false } : path === "/api/evobgp/settings" ? { enabled: false, baseUrl: "", secretConfigured: false } : path === "/api/filters/rules" ? { rulesets: [{ serverId: "1", rules: ids.map(i => ({ id: String(i), community: `65001:${i}`, communityName: `Сервис ${i}`, gateway: "192.0.2.2", gatewayTunnelId: "", description: "" })) }] } @@ -84,3 +84,19 @@ test("Оптимизатор: без ExitNode отсутствующие инд await expect(grid).not.toContainText("0 б.") await expect(grid.getByRole("button",{name:"Симулировать",exact:true})).toHaveCount(0) }) + + +test("Оптимизатор: закрепление Community сохраняется между страницами устройств", async ({ page }) => { + await setup(page, "ok", false, true) + const scope = page.getByRole("region", { name: "Домашние устройства оптимизатора" }) + await page.getByRole("button", { name: "BGP community (12)", exact: true }).click() + const rec = scope.getByRole("table").filter({ hasText: "Community" }).locator('tr[data-row-id="1::65001:1::0"]') + await rec.getByRole("button", { name: "Закрепить", exact: true }).click() + await expect(rec.getByRole("button", { name: "Открепить", exact: true })).toBeVisible() + await scope.getByRole("button", { name: "Следующая страница домашних устройств" }).click() + await expect(scope.getByRole("status")).toContainText("6–6 из 6") + await expect(scope.getByText("Другой дом 8", { exact: true })).toBeVisible() + await scope.getByRole("button", { name: "Предыдущая страница домашних устройств" }).click() + await page.getByRole("button", { name: "BGP community (12)", exact: true }).click() + await expect(rec.getByRole("button", { name: "Открепить", exact: true })).toBeVisible() +})