fix(ui): отделить измерения цепочек от модели
This commit is contained in:
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>
|
||||
|
||||
@@ -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-модель.
|
||||
|
||||
@@ -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 ещё требуют обновления.
|
||||
@@ -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,
|
||||
|
||||
@@ -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 модели")
|
||||
})
|
||||
Reference in new issue
Block a user