fix(ui): отделить измерения цепочек от модели

This commit is contained in:
Denozordec committed 2026-10-01 00:41:54 +07:00
1 parent 9404bd5136
commit 255f272fab
7 files changed
+82 -13

No files matched your search

@@ -108,7 +108,7 @@ function RouteOptimizerFullRoutesDataGrid({
<div>
<div className="font-medium">{r.jh.label}</div>
<div className="font-mono text-[10px] text-muted-foreground">
{r.hw.pingMs} мс · ↓{r.hw.dlMbps} ↑{r.hw.ulMbps}
Модель: {r.hw.pingMs} мс · ↓{r.hw.dlMbps} ↑{r.hw.ulMbps}
</div>
</div>
</div>
@@ -130,7 +130,7 @@ function RouteOptimizerFullRoutesDataGrid({
<span className="text-[10px] text-muted-foreground">({r.exit.site})</span>
</div>
<div className="font-mono text-[10px] text-muted-foreground">
{r.je.pingMs} мс · ↓{r.je.dlMbps} ↑{r.je.ulMbps}
Модель: {r.je.pingMs} мс · ↓{r.je.dlMbps} ↑{r.je.ulMbps}
</div>
</div>
)
@@ -141,7 +141,7 @@ function RouteOptimizerFullRoutesDataGrid({
id: "totalPing",
accessorFn: (row) => row.hw.pingMs + row.je.pingMs,
header: ({ column }) => (
<DataGridSortHeader column={column} title="Ping (итого)" className="mx-auto" />
<DataGridSortHeader column={column} title="Ping модели" className="mx-auto" />
),
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 }) => (
<DataGridSortHeader column={column} title="Conf." className="mx-auto" />
<DataGridSortHeader column={column} title="Категория модели" className="mx-auto" />
),
cell: ({ row }) => (
<div className="flex justify-center">
@@ -239,7 +239,7 @@ function RouteOptimizerFullRoutesDataGrid({
</div>
),
meta: {
headerTitle: "Conf.",
headerTitle: "Категория модели",
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: cn(DATA_GRID_CELL_PAD_LAST, "text-center"),
},
@@ -129,21 +129,24 @@ function RouteOptimizerWanMatrixDataGrid({
<span
className={cn(
"font-mono text-xs font-semibold",
leg.pingMs < 10
leg.observed && leg.observed.pingMs === null ? "text-muted-foreground" : (leg.observed?.pingMs ?? leg.pingMs) < 10
? "text-success "
: leg.pingMs < 25
: (leg.observed?.pingMs ?? leg.pingMs) < 25
? "text-foreground"
: "text-warning ",
)}
>
{leg.pingMs} мс
{leg.observed ? (leg.observed.pingMs ?? "—") : leg.pingMs} мс
</span>
<span className="text-[10px] text-muted-foreground font-mono">
↓{leg.dlMbps} ↑{leg.ulMbps}
↓{leg.observed ? (leg.observed.dlMbps ?? "—") : leg.dlMbps} ↑{leg.observed ? (leg.observed.ulMbps ?? "—") : leg.ulMbps}
</span>
{leg.observed && <span className="text-[10px] text-muted-foreground">
{leg.observed.probeId ? <>Снимок пробы · Ping: {leg.observed.pingAt ? <time dateTime={leg.observed.pingAt}>{new Date(leg.observed.pingAt).toLocaleString("ru-RU")}</time> : "дата неизвестна"}. Скорость: дата неизвестна.</> : "Измерения отсутствуют"}
</span>}
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] font-mono text-foreground/70">score {leg.score}</span>
{leg.loss > 0 && <LossChip loss={leg.loss} />}
<span className="text-[10px] font-mono text-foreground/70">модель {leg.score}</span>
{(leg.observed ? (leg.observed.lossPct ?? 0) : leg.loss) > 0 && <LossChip loss={leg.observed?.lossPct ?? leg.loss} />}
</div>
</div>
</div>
+8 -1
View File
@@ -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-модель.
+4
View File
@@ -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 ещё требуют обновления.
+17
View File
@@ -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,
+23
View File
@@ -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)
}
})
@@ -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 модели")
})