fix(ui): не подменять неизвестный индекс маршрута нулём
This commit is contained in:
1 parent
fb70d80220
commit
2cdfa2cb13
6 files changed
+40
-10
No files matched your search
@@ -32,7 +32,8 @@ function Chip({ children, color }: { children: React.ReactNode; color?: string }
|
||||
)
|
||||
}
|
||||
|
||||
function ProbChip({ prob, best }: { prob: number; best?: boolean }) {
|
||||
function ProbChip({ prob, best }: { prob: number | null; best?: boolean }) {
|
||||
if (prob === null) return <span className="text-muted-foreground" aria-label="Индекс неизвестен">—</span>
|
||||
return (
|
||||
<Chip
|
||||
color={
|
||||
@@ -141,9 +142,9 @@ function RouteOptimizerCommRecsDataGrid({
|
||||
<span>{r.recommended.jh}</span>
|
||||
<ArrowRightIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
|
||||
<span>{r.recommended.exit}</span>
|
||||
{r.shouldSwitch && !isPinned && (
|
||||
{r.shouldSwitch && r.current && !isPinned && (
|
||||
<span className="ml-1 text-[10px] font-bold bg-warning/10 border border-warning/20 px-1.5 py-0.5 rounded">
|
||||
+{(r.recommended.prob ?? 0) - (r.current?.prob ?? 0)} б.
|
||||
+{r.recommended.prob - r.current.prob} б.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -164,9 +165,9 @@ function RouteOptimizerCommRecsDataGrid({
|
||||
const pinKey = `${homeId}::${r.community}`
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<ProbChip prob={r.current?.prob ?? 0} />
|
||||
<ProbChip prob={r.current?.prob ?? null} />
|
||||
<span className="text-muted-foreground text-[10px]">/</span>
|
||||
<ProbChip prob={r.recommended?.prob ?? 0} best={r.shouldSwitch && !pinned.has(pinKey)} />
|
||||
<ProbChip prob={r.recommended?.prob ?? null} best={r.shouldSwitch && !pinned.has(pinKey)} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -26,11 +26,17 @@
|
||||
| 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.34; адаптивность оптимизатора проверена, далее достоверность индексов/контекст |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.35; отсутствующий индекс не подменяется нулём, далее URL/source-контекст |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
## Блок 10.35 — неизвестные индексы Community (2026-09-30)
|
||||
|
||||
- Если полной цепочки нет, оба индекса показывают «—» с доступным названием; отсутствующая база не участвует в вычислении отображаемого выигрыша. Переключение не рекомендуется даже при нулевом пороге. Формула ранжирования полных цепочек не менялась.
|
||||
- Проверки: 7 E2E (включая отсутствие ExitNode, полные таблицы и симуляцию), 3 unit (включая нулевой порог), TypeScript и профильный lint прошли. Production frontend был проверен в предыдущем блоке; общая основа не менялась.
|
||||
- Далее: URL/source-контекст оптимизатора. Model fallback/clamp отдельно обозначить, затем карта/терминал; реальная RouterOS/БД приёмка внешняя.
|
||||
|
||||
## Блок 10.34 — адаптивность и доступность оптимизатора (2026-09-30)
|
||||
|
||||
- KPI читаются на 320/390 px: одна колонка на телефоне, две/четыре при достаточной ширине контейнера. Визуальная проверка выявила скрытые KPI внутри native flex-fieldset; внутренний flex-контейнер восстановил layout без изменения общего KpiStatGrid.
|
||||
|
||||
@@ -188,3 +188,7 @@ Observed selector вместо default cost/disabled; неоднозначные
|
||||
## 10.34. Адаптивность оптимизатора
|
||||
|
||||
1440/390/320 px, keyboard/ARIA и disabled ECMP; native fieldset композиция исправлена после визуального обнаружения невидимых KPI. Семантические токены четырёх таблиц, девять снимков в плане; 35 E2E, TS/lint и production frontend прошли. Teardown catalog отказ backend записан в журнале. URL/source и model provenance ещё открыты.
|
||||
|
||||
## 10.35. Отсутствующие индексы Community
|
||||
|
||||
Без полной цепочки индексы неизвестны, рекомендации нет даже при пороге 0. 7 E2E, 3 unit, TS/lint прошли; расчёт существующих цепочек прежний. Далее URL/source-контекст.
|
||||
@@ -559,13 +559,13 @@ export function buildLiveOptimizerData(
|
||||
prob: curProb,
|
||||
}
|
||||
: null
|
||||
const diff = (rec?.prob ?? 0) - (cur?.prob ?? 0)
|
||||
const diff = rec && cur ? rec.prob - cur.prob : null
|
||||
return {
|
||||
community: rule.community,
|
||||
communityName: rule.communityName ?? rule.community,
|
||||
current: cur,
|
||||
recommended: rec,
|
||||
shouldSwitch: diff >= settings.switchThreshold,
|
||||
shouldSwitch: diff !== null && diff >= settings.switchThreshold,
|
||||
pinnedBySettings: false,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,3 +16,12 @@ test("optimizer refuses unknown types, duplicate inventory/WAN and invalid metri
|
||||
for(const value of [[{...server,type:undefined}],[server,server],[{...server,latency:-1}],[{...server,wanUplinks:[{id:"wan",name:"WAN",isp:"ISP",iface:"ether1",ip:"",maxDl:0,maxUl:10}]}]]) assert.ok(!optimizerServersSchema.safeParse(value).success)
|
||||
for(const value of [[probe,probe],[{...probe,lastPingRttMs:-1}],[{...probe,lastPingLossPct:101}],[{...probe,lastPingAt:"yesterday"}],[{...probe,lastTxAvgMbps:undefined}]]) assert.ok(!optimizerProbesSchema.safeParse({probes:value}).success)
|
||||
})
|
||||
|
||||
import {buildLiveOptimizerData,DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS} from "./route-optimizer-data"
|
||||
test("optimizer does not recommend an absent chain at a zero threshold",()=>{
|
||||
const rows=[{...server,wanUplinks:[{id:"wan",name:"WAN",isp:"ISP",iface:"ether1",ip:"198.51.100.1",maxDl:100,maxUl:100}]}]
|
||||
const rules=optimizerRulesSchema.parse({rulesets:[{serverId:"1",rules:[{id:"r",community:"65001:1",communityName:"Service",gateway:"192.0.2.2",gatewayTunnelId:"",description:""}]}]}).rulesets
|
||||
const result=buildLiveOptimizerData(rows,rules,{...DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS,switchThreshold:0})
|
||||
const recommendation=result.homes[0].commRecs[0]
|
||||
assert.equal(recommendation.current,null);assert.equal(recommendation.recommended,null);assert.equal(recommendation.shouldSwitch,false)
|
||||
})
|
||||
@@ -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") {
|
||||
async function setup(page: Page, preview: "ok" | "empty" | "error" = "ok", missingExit = 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") {
|
||||
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
|
||||
return route.fulfill({ json: path === "/api/servers" ? servers.filter(server => !missingExit || server.type !== "exit-node")
|
||||
: 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: "" })) }] }
|
||||
@@ -74,3 +74,13 @@ for (const state of ["empty", "error"] as const) {
|
||||
await expect(page.getByRole("table").filter({ hasText: "Speed (dl/ul)" })).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
test("Оптимизатор: без ExitNode отсутствующие индексы не становятся нулями", async ({page}) => {
|
||||
await setup(page,"ok",true)
|
||||
await page.getByRole("button",{name:"BGP community (12)",exact:true}).click()
|
||||
const grid=page.getByRole("table").filter({hasText:"Community"})
|
||||
await expect(grid.locator("tr[data-row-id]")).toHaveCount(12)
|
||||
await expect(grid.getByLabel("Индекс неизвестен")).toHaveCount(24)
|
||||
await expect(grid).not.toContainText("0 б.")
|
||||
await expect(grid.getByRole("button",{name:"Симулировать",exact:true})).toHaveCount(0)
|
||||
})
|
||||
Reference in new issue
Block a user