fix(ui): исключить подстановку моковых WAN в live расчёт
This commit is contained in:
1 parent
338335ddc0
commit
6e6558bde9
7 files changed
+35
-41
No files matched your search
@@ -346,6 +346,7 @@ function HomeRouterCard({ entry, jumpHosts, pinned, applied, applying, onPin, on
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{home.wans.length === 0 && <p className="px-5 py-3 text-xs text-muted-foreground">Нет WAN-аплинков для расчёта. <Link className="underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring" href={"/servers?server="+encodeURIComponent(home.id)}>Настроить сервер</Link></p>}
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
{tab === "wan-matrix" && (
|
{tab === "wan-matrix" && (
|
||||||
<RouteOptimizerWanMatrixDataGrid home={home} legs={wanJhLegs} jumpHosts={jumpHosts} />
|
<RouteOptimizerWanMatrixDataGrid home={home} legs={wanJhLegs} jumpHosts={jumpHosts} />
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function RouteOptimizerWanMatrixDataGrid({
|
|||||||
id: "bandwidth",
|
id: "bandwidth",
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="text-[11px] font-medium text-muted-foreground block text-right">
|
<span className="text-[11px] font-medium text-muted-foreground block text-right">
|
||||||
Макс. полоса
|
Лимит модели
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -26,11 +26,17 @@
|
|||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.41; Router ID проверяется без first-match, далее визуальная приёмка |
|
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.42; live WAN без моков/синтетики, далее подписи и локальная приёмка |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 10.42 — WAN только из live каталога (2026-10-01)
|
||||||
|
|
||||||
|
- Live topology не ищет WAN по совпадению name/host/id в lib/data и не создаёт WAN1/1000 автоматически. Нет uplinks — нет цепочек/лучшего маршрута; пояснение ведёт к /servers?server=<id>. Для существующих WAN формула ранжирования прежняя; лимит модели явно назван расчётным.
|
||||||
|
- 6 unit (включая совпадение с mock inventory), 16 E2E measured/unknown/empty WAN, read failures/races и simulation, TS/lint прошли.
|
||||||
|
- Далее уточнить единицы порога/инертный гистерезис, scope Community по домашнему серверу, затем обновить screenshots и принять оптимизатор локально.
|
||||||
|
|
||||||
## Блок 10.41 — однозначное устройство OSPF (2026-10-01)
|
## Блок 10.41 — однозначное устройство OSPF (2026-10-01)
|
||||||
|
|
||||||
- Сопоставление Router ID больше не останавливается на первом совпадении. Проверяются все enabled устройства: нужен literal ID активного экземпляра, повтор на другом устройстве отклоняется; неизвестный disabled/ID либо отказ опроса сообщает ошибку до write. Повторы одного ID внутри одного устройства допустимы; known disabled исключены. Без соседских Router ID дополнительный сетевой опрос не нужен.
|
- Сопоставление Router ID больше не останавливается на первом совпадении. Проверяются все enabled устройства: нужен literal ID активного экземпляра, повтор на другом устройстве отклоняется; неизвестный disabled/ID либо отказ опроса сообщает ошибку до write. Повторы одного ID внутри одного устройства допустимы; known disabled исключены. Без соседских Router ID дополнительный сетевой опрос не нужен.
|
||||||
|
|||||||
@@ -216,3 +216,7 @@ Shared контракт и backend observed исходные значения/д
|
|||||||
## 10.41. Однозначность Router ID
|
## 10.41. Однозначность Router ID
|
||||||
|
|
||||||
Полный опрос enabled inventory без first-match; duplicate ownership/unknown/failed каталог отклоняет план до write. Known disabled/empty и одинаковый ID одного устройства допустимы. Optimizer suite/backend build прошли, lint два прежних warning. Реальный RouterOS каталог ещё внешняя приёмка; далее screenshots.
|
Полный опрос enabled inventory без first-match; duplicate ownership/unknown/failed каталог отклоняет план до write. Known disabled/empty и одинаковый ID одного устройства допустимы. Optimizer suite/backend build прошли, lint два прежних warning. Реальный RouterOS каталог ещё внешняя приёмка; далее screenshots.
|
||||||
|
|
||||||
|
## 10.42. Live WAN без подстановки
|
||||||
|
|
||||||
|
Нет mock catalog/synthetic WAN в live topology; отсутствие uplinks объяснено с переходом к серверу. 6 unit, 16 E2E, TS/lint прошли. Далее подписи и Community scope перед визуальной приёмкой.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
* (см. блок `#route-ai` в `/settings` и страницу `/route-optimizer`).
|
* (см. блок `#route-ai` в `/settings` и страницу `/route-optimizer`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { servers, type FilterRule } from "@/lib/data"
|
import { type FilterRule } from "@/lib/data"
|
||||||
|
|
||||||
// ── Совместимо с app/(main)/route-optimizer/page.tsx ─────────────────────────
|
// ── Совместимо с app/(main)/route-optimizer/page.tsx ─────────────────────────
|
||||||
|
|
||||||
@@ -191,18 +191,6 @@ function confidenceFromProb(prob: number): "HIGH" | "MEDIUM" | "LOW" {
|
|||||||
return prob >= 55 ? "HIGH" : prob >= 30 ? "MEDIUM" : "LOW"
|
return prob >= 55 ? "HIGH" : prob >= 30 ? "MEDIUM" : "LOW"
|
||||||
}
|
}
|
||||||
|
|
||||||
function syntheticWans(home: OptimizerApiServer): WanUplink[] {
|
|
||||||
return [{
|
|
||||||
id: `w-${home.id}-1`,
|
|
||||||
name: "WAN1",
|
|
||||||
isp: "—",
|
|
||||||
iface: "auto",
|
|
||||||
ip: home.host,
|
|
||||||
maxDl: 1000,
|
|
||||||
maxUl: 1000,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeApiWanUplinks(list: OptimizerApiServer["wanUplinks"]): WanUplink[] {
|
function normalizeApiWanUplinks(list: OptimizerApiServer["wanUplinks"]): WanUplink[] {
|
||||||
const src = Array.isArray(list) ? list : []
|
const src = Array.isArray(list) ? list : []
|
||||||
const out = src
|
const out = src
|
||||||
@@ -219,29 +207,6 @@ function normalizeApiWanUplinks(list: OptimizerApiServer["wanUplinks"]): WanUpli
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
function catalogWansForHome(homeId: string, homeHost?: string, homeName?: string): WanUplink[] | null {
|
|
||||||
const hostNorm = (homeHost ?? "").trim().toLowerCase()
|
|
||||||
const nameNorm = (homeName ?? "").trim().toLowerCase()
|
|
||||||
const row = servers.find((s) => {
|
|
||||||
if (s.type !== "home-router") return false
|
|
||||||
if (String(s.id) === homeId) return true
|
|
||||||
if (hostNorm && String(s.host ?? "").trim().toLowerCase() === hostNorm) return true
|
|
||||||
if (nameNorm && String(s.name ?? "").trim().toLowerCase() === nameNorm) return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
const list = row?.wanUplinks ?? []
|
|
||||||
if (!list.length) return null
|
|
||||||
return list.map((w, idx) => ({
|
|
||||||
id: w.id || `w-${homeId}-${idx + 1}`,
|
|
||||||
name: w.name || `WAN${idx + 1}`,
|
|
||||||
isp: w.isp || "—",
|
|
||||||
iface: w.iface || "",
|
|
||||||
ip: w.ip || "",
|
|
||||||
maxDl: Math.max(1, Math.round(Number(w.maxDl) || 1000)),
|
|
||||||
maxUl: Math.max(1, Math.round(Number(w.maxUl) || 1000)),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function legPing(
|
function legPing(
|
||||||
a: number | null,
|
a: number | null,
|
||||||
b: number | null,
|
b: number | null,
|
||||||
@@ -285,9 +250,7 @@ export function mapApiServersToTopology(rows: OptimizerApiServer[]): {
|
|||||||
model: s.model ?? "—",
|
model: s.model ?? "—",
|
||||||
ip: s.host,
|
ip: s.host,
|
||||||
wans:
|
wans:
|
||||||
normalizeApiWanUplinks(s.wanUplinks).length > 0
|
normalizeApiWanUplinks(s.wanUplinks),
|
||||||
? normalizeApiWanUplinks(s.wanUplinks)
|
|
||||||
: (catalogWansForHome(String(s.id), s.host, s.name) ?? syntheticWans(s)),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const jumpHosts: JumpHost[] = rows
|
const jumpHosts: JumpHost[] = rows
|
||||||
|
|||||||
@@ -21,3 +21,14 @@ test("missing and disabled probe leave observed values unknown while model stays
|
|||||||
assert.equal(result.homes[0].fullRoutes[0].je.observed?.probeId,null)
|
assert.equal(result.homes[0].fullRoutes[0].je.observed?.probeId,null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
import {servers} from "./data"
|
||||||
|
test("live topology never imports mock WAN or creates a synthetic uplink",()=>{
|
||||||
|
const mock=servers.find(server=>server.type==="home-router")!
|
||||||
|
for(const wanUplinks of [undefined,[]]){
|
||||||
|
const inventory=rows.map((row,i)=>i===0?{...row,name:mock.name,host:mock.host,wanUplinks}:row)
|
||||||
|
const result=buildLiveOptimizerData(inventory,[],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS)
|
||||||
|
assert.equal(result.homes[0].home.wans.length,0);assert.equal(result.homes[0].fullRoutes.length,0)
|
||||||
|
assert.equal(result.homes[0].bestRoute,null)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -27,3 +27,12 @@ test("Оптимизатор: OSPF исходный ноль и происхож
|
|||||||
await expect(grid).toContainText("Измерение: —")
|
await expect(grid).toContainText("Измерение: —")
|
||||||
await expect(grid).toContainText("Трафик интерфейса: 0 Мбит/с")
|
await expect(grid).toContainText("Трафик интерфейса: 0 Мбит/с")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Оптимизатор: WAN не подставляется из демо",async({page})=>{
|
||||||
|
await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
|
||||||
|
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"?[{id:1,name:"Router",host:"192.0.2.1",type:"home-router",country:"RU",enabled:true,status:null}]:p==="/api/filters/rules"?{rulesets:[]}:p==="/api/uptime/speed-probes"?{probes:[]}:p==="/api/ospf/catalog"?{interfaces:[],instances:[],neighbors:[],bfdSessions:[],routes:[],failures:[]}:p.endsWith("/preview")?{serverId:1,serverName:"Router",pingWeight:60,interfacesTotal:0,changedCount:0,unchangedCount:0,interfaces:[],changes:[]}:{}})})
|
||||||
|
await page.goto("/route-optimizer")
|
||||||
|
await expect(page.getByText("Нет WAN-аплинков для расчёта.",{exact:false})).toBeVisible()
|
||||||
|
await expect(page.getByRole("table").filter({hasText:"WAN-аплинк"}).locator("tr[data-row-id]")).toHaveCount(0)
|
||||||
|
await expect(page.getByRole("link",{name:"Настроить сервер",exact:true})).toHaveAttribute("href","/servers?server=1")
|
||||||
|
})
|
||||||
Reference in new issue
Block a user