From 255f272fabf45dcbc5cd884884336af1b98afd03 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 00:41:54 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=86=D0=B5=D0=BF=D0=BE=D1=87=D0=B5=D0=BA=20=D0=BE?= =?UTF-8?q?=D1=82=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route-optimizer-full-routes-data-grid.tsx | 12 +++++----- .../route-optimizer-wan-matrix-data-grid.tsx | 15 +++++++----- docs/ux-ui-progress.md | 9 +++++++- docs/ux-ui-stage-10.md | 4 ++++ lib/route-optimizer-data.ts | 17 ++++++++++++++ lib/route-optimizer-observed.test.ts | 23 +++++++++++++++++++ tests/e2e/route-optimizer-observed.spec.ts | 15 ++++++++++++ 7 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 lib/route-optimizer-observed.test.ts create mode 100644 tests/e2e/route-optimizer-observed.spec.ts diff --git a/components/data-grids/route-optimizer-full-routes-data-grid.tsx b/components/data-grids/route-optimizer-full-routes-data-grid.tsx index 60cfb5d3..d306c9ca 100644 --- a/components/data-grids/route-optimizer-full-routes-data-grid.tsx +++ b/components/data-grids/route-optimizer-full-routes-data-grid.tsx @@ -108,7 +108,7 @@ function RouteOptimizerFullRoutesDataGrid({
{r.jh.label}
- {r.hw.pingMs} мс · ↓{r.hw.dlMbps} ↑{r.hw.ulMbps} + Модель: {r.hw.pingMs} мс · ↓{r.hw.dlMbps} ↑{r.hw.ulMbps}
@@ -130,7 +130,7 @@ function RouteOptimizerFullRoutesDataGrid({ ({r.exit.site})
- {r.je.pingMs} мс · ↓{r.je.dlMbps} ↑{r.je.ulMbps} + Модель: {r.je.pingMs} мс · ↓{r.je.dlMbps} ↑{r.je.ulMbps}
) @@ -141,7 +141,7 @@ function RouteOptimizerFullRoutesDataGrid({ id: "totalPing", accessorFn: (row) => row.hw.pingMs + row.je.pingMs, header: ({ column }) => ( - + ), cell: ({ row }) => { const totalPing = row.original.hw.pingMs + row.original.je.pingMs @@ -161,7 +161,7 @@ function RouteOptimizerFullRoutesDataGrid({ ) }, meta: { - headerTitle: "Ping (итого)", + headerTitle: "Ping модели", headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"), cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"), }, @@ -231,7 +231,7 @@ function RouteOptimizerFullRoutesDataGrid({ id: "confidence", accessorKey: "confidence", header: ({ column }) => ( - + ), cell: ({ row }) => (
@@ -239,7 +239,7 @@ function RouteOptimizerFullRoutesDataGrid({
), meta: { - headerTitle: "Conf.", + headerTitle: "Категория модели", headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: cn(DATA_GRID_CELL_PAD_LAST, "text-center"), }, diff --git a/components/data-grids/route-optimizer-wan-matrix-data-grid.tsx b/components/data-grids/route-optimizer-wan-matrix-data-grid.tsx index e5df92f8..1b15f9ad 100644 --- a/components/data-grids/route-optimizer-wan-matrix-data-grid.tsx +++ b/components/data-grids/route-optimizer-wan-matrix-data-grid.tsx @@ -129,21 +129,24 @@ function RouteOptimizerWanMatrixDataGrid({ - {leg.pingMs} мс + {leg.observed ? (leg.observed.pingMs ?? "—") : leg.pingMs} мс - ↓{leg.dlMbps} ↑{leg.ulMbps} + ↓{leg.observed ? (leg.observed.dlMbps ?? "—") : leg.dlMbps} ↑{leg.observed ? (leg.observed.ulMbps ?? "—") : leg.ulMbps} + {leg.observed && + {leg.observed.probeId ? <>Снимок пробы · Ping: {leg.observed.pingAt ? : "дата неизвестна"}. Скорость: дата неизвестна. : "Измерения отсутствуют"} + }
- score {leg.score} - {leg.loss > 0 && } + модель {leg.score} + {(leg.observed ? (leg.observed.lossPct ?? 0) : leg.loss) > 0 && }
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index b71ba88f..2e235211 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -26,11 +26,18 @@ | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже | | 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 | | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | -| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.38; preview явно расчётный, далее provenance полей | +| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.39; исходные WAN/JH метрики отделены от модели, далее backend OSPF provenance | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 10.39 — исходные метрики цепочек оптимизатора (2026-10-01) + +- Live WAN–JH/JH–Exit сохраняют observed record отдельно от model полей. Исходные 0, дроби, null и дата пробы не округляются/не ограничиваются WAN cap. Расчётные поля и ranking остаются прежними. +- Матрица показывает исходные измерения или «—»; без пробы не показывает вычисленный Ping/скорость как измерение. Дата Ping видима, время скорости неизвестно (API его не передаёт), давние пробы не названы текущими. Полные цепочки подписаны как модель, confidence — категория модели. Моки сохраняют отдельный демонстрационный режим. +- Проверки: 2 unit (нули/дроби/null/давняя дата/disabled/missing, сохранение model clamp/score), 9 E2E (2 новых measured/unknown плюс таблицы/симуляция), TS/lint прошли. ReUI audit consumer-композиции без vendor/theme изменений. +- Далее backend OSPF observed provenance и строгий контракт. Снимки 10.34 пока не обновлены; визуальную приёмку повторить после backend блока. RouterOS/БД операции по-прежнему не проверены. + ## Блок 10.38 — пояснение метрик OSPF preview (2026-09-30) - Рядом с устройством preview явно сообщает: Ping/скорость могут включать расчётные значения и не подтверждают текущее качество соединения. Значения, ranking и write policy не менялись. Это обозначение ограничения, а не завершённая provenance-модель. diff --git a/docs/ux-ui-stage-10.md b/docs/ux-ui-stage-10.md index 750c6832..1a68cabe 100644 --- a/docs/ux-ui-stage-10.md +++ b/docs/ux-ui-stage-10.md @@ -204,3 +204,7 @@ server сохраняет выбор/reload/history без потери друг ## 10.38. Смысл метрик preview Короткое пояснение расчётных Ping/скорости, без изменения ranking/write. Lint и 14 preview E2E прошли. Далее provenance полей с сохранением observed 0/null; снимки 10.34 ещё без нового текста. Оптимизатор/этап 10 целиком не приняты. + +## 10.39. Observed метрики WAN–JH/JH–Exit + +Исходные значения/дата пробы отдельно от неизменной модели. Матрица не подменяет unknown моделью, полные цепочки явно расчётные. 2 unit, 9 E2E, TS/lint и ReUI audit прошли. Далее backend OSPF provenance; screenshots ещё требуют обновления. diff --git a/lib/route-optimizer-data.ts b/lib/route-optimizer-data.ts index 5bb0d3e8..bb5728ee 100644 --- a/lib/route-optimizer-data.ts +++ b/lib/route-optimizer-data.ts @@ -43,7 +43,21 @@ export interface ExitNode { ip: string } +export interface OptimizerObservedMetrics { + probeId: string | null + pingMs: number | null + lossPct: number | null + dlMbps: number | null + ulMbps: number | null + pingAt: string | null +} + +export function readOptimizerObservedMetrics(probe?: RouteOptimizerSpeedProbe): OptimizerObservedMetrics { + return {probeId:probe?.id ?? null,pingMs:probe?.lastPingRttMs ?? null,lossPct:probe?.lastPingLossPct ?? null,dlMbps:probe?.lastTxAvgMbps ?? null,ulMbps:probe?.lastRxAvgMbps ?? null,pingAt:probe?.lastPingAt ?? null} +} + export interface WanJhLeg { + observed?: OptimizerObservedMetrics wanId: string jhId: string pingMs: number @@ -54,6 +68,7 @@ export interface WanJhLeg { } export interface JhExLeg { + observed?: OptimizerObservedMetrics jhId: string exitId: string pingMs: number @@ -464,6 +479,7 @@ export function buildLiveOptimizerData( ? Math.max(0, Math.min(100, Math.round(probe.lastPingLossPct))) : (!homeOnline || !jhOnline ? 100 : 0) wanJhLegs.push({ + observed: readOptimizerObservedMetrics(probe), wanId: wan.id, jhId: jh.id, pingMs: ping, @@ -494,6 +510,7 @@ export function buildLiveOptimizerData( ? Math.max(1, Math.round(probe.lastRxAvgMbps)) : modelBw.ul jhExMap.set(`${jh.id}::${ex.id}`, { + observed: readOptimizerObservedMetrics(probe), jhId: jh.id, exitId: ex.id, pingMs: ping, diff --git a/lib/route-optimizer-observed.test.ts b/lib/route-optimizer-observed.test.ts new file mode 100644 index 00000000..c61fcc60 --- /dev/null +++ b/lib/route-optimizer-observed.test.ts @@ -0,0 +1,23 @@ +import {test} from "node:test" +import assert from "node:assert/strict" +import {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,readOptimizerObservedMetrics,type OptimizerApiServer,type RouteOptimizerSpeedProbe,calcRouteScore} from "./route-optimizer-data" +const rows:OptimizerApiServer[]=(["home-router","jump-host","exit-node"] as const).map((type,i)=>({id:i+1,name:"Router"+i,host:"192.0.2."+i,country:"RU",site:"TEST",model:"CHR",type,enabled:true,status:"online",latency:10,...(i===0?{wanUplinks:[{id:"w",name:"WAN",iface:"ether1",isp:"ISP",ip:"198.51.100.1",maxDl:100,maxUl:100}]}:{})})) +const probe:RouteOptimizerSpeedProbe={id:"p",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"ether2",enabled:true,lastPingRttMs:0,lastPingLossPct:0,lastTxAvgMbps:150.25,lastRxAvgMbps:null,lastPingAt:"2020-01-01T00:00:00Z"} +test("observed raw zero, decimal, null and old receipt stay separate from model clamp",()=>{ + const result=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,[probe,{...probe,id:"e",srcServerId:"2",dstServerId:"3",lastTxAvgMbps:0}]) + const leg=result.homes[0].wanJhLegs[0],exit=result.homes[0].fullRoutes[0].je + assert.equal(leg.pingMs,1);assert.equal(leg.dlMbps,100) + assert.equal(leg.observed?.pingMs,0);assert.equal(leg.observed?.dlMbps,150.25);assert.equal(leg.observed?.ulMbps,null) + assert.equal(leg.observed?.lossPct,0);assert.equal(leg.observed?.pingAt,"2020-01-01T00:00:00Z") + assert.equal(leg.score,calcRouteScore(leg.pingMs,leg.dlMbps,leg.ulMbps,60)) + assert.equal(exit.pingMs,1);assert.equal(exit.observed?.pingMs,0);assert.equal(exit.observed?.dlMbps,0) +}) +test("missing and disabled probe leave observed values unknown while model stays available",()=>{ + for(const probes of [[],[{...probe,enabled:false}]]){ + const result=buildLiveOptimizerData(rows,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,probes) + const leg=result.homes[0].wanJhLegs[0] + assert.deepEqual(leg.observed,readOptimizerObservedMetrics()) + assert.ok(leg.pingMs>0);assert.ok(leg.dlMbps>0) + assert.equal(result.homes[0].fullRoutes[0].je.observed?.probeId,null) + } +}) diff --git a/tests/e2e/route-optimizer-observed.spec.ts b/tests/e2e/route-optimizer-observed.spec.ts new file mode 100644 index 00000000..089f5b07 --- /dev/null +++ b/tests/e2e/route-optimizer-observed.spec.ts @@ -0,0 +1,15 @@ +import {test,expect} from "@playwright/test" +for(const hasProbe of [true,false])test("Оптимизатор: исходные измерения без clamp/"+hasProbe,async({page})=>{ + await page.route("**/health",r=>r.fulfill({json:{ok:true}})) + const probe={id:"p",srcServerId:"1",dstServerId:"2",srcInterface:"ether1",dstInterface:"ether2",enabled:true,lastPingRttMs:0,lastPingLossPct:0,lastTxAvgMbps:150.25,lastRxAvgMbps:null,lastPingAt:"2020-01-01T00:00:00Z"} + await page.route("**/api/**",r=>{const p=new URL(r.request().url()).pathname;return r.fulfill({json:p==="/api/auth/config"?{required:false}:p==="/api/servers"?["home-router","jump-host","exit-node"].map((type,i)=>({id:i+1,name:"Router"+i,host:"192.0.2."+i,type,country:"RU",enabled:true,status:"online",latency:10,...(i===0?{wanUplinks:[{id:"w",name:"WAN",iface:"ether1",isp:"ISP",ip:"198.51.100.1",maxDl:100,maxUl:100}]}:{})})):p==="/api/filters/rules"?{rulesets:[]}:p==="/api/uptime/speed-probes"?{probes:hasProbe?[probe]:[]}:p==="/api/ospf/catalog"?{interfaces:[],instances:[],neighbors:[],bfdSessions:[],routes:[],failures:[]}:p.endsWith("/preview")?{serverId:1,serverName:"Router0",pingWeight:60,interfacesTotal:0,changedCount:0,unchangedCount:0,interfaces:[],changes:[]}:{}})}) + await page.goto("/route-optimizer") + const matrix=page.getByRole("table").filter({hasText:"WAN-аплинк"}) + await expect(matrix.locator("tr[data-row-id]")).toHaveCount(1) + await expect(matrix).toContainText(hasProbe?"0 мс":"— мс") + await expect(matrix).toContainText(hasProbe?"↓150.25 ↑—":"↓— ↑—") + await expect(matrix).toContainText(hasProbe?"Скорость: дата неизвестна":"Измерения отсутствуют") + if(hasProbe)await expect(matrix.locator("time")).toHaveAttribute("datetime","2020-01-01T00:00:00Z") + await page.getByRole("button",{name:"Маршруты (1)",exact:true}).click() + await expect(page.getByRole("table").filter({hasText:"# Маршрут"})).toContainText("Ping модели") +})