fix(ui): сделать сводки площадок краткими и достоверными
This commit is contained in:
1 parent
52d8a05480
commit
53e36f2a03
9 files changed
+86
-4
No files matched your search
@@ -7,6 +7,7 @@ import {can} from "@/lib/auth"
|
||||
import {ospfCatalogSchema,ospfOptimizeSchema} from "@/lib/ospf-response"
|
||||
import {optimizerServersSchema,optimizerRulesSchema,optimizerProbesSchema,optimizerOspfPreviewSchema,type OptimizerOspfPreview} from "@/lib/route-optimizer-response"
|
||||
import { OptimizerHomesList } from "@/components/optimizer-homes-list"
|
||||
import { OptimizerSiteSummary } from "@/components/optimizer-site-summary"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { RouteOptimizerWanMatrixDataGrid } from "@/components/data-grids/route-optimizer-wan-matrix-data-grid"
|
||||
import { RouteOptimizerFullRoutesDataGrid } from "@/components/data-grids/route-optimizer-full-routes-data-grid"
|
||||
@@ -588,8 +589,8 @@ function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer
|
||||
: HOME_ROUTERS.reduce((s, h) => s + h.wans.length, 0)
|
||||
const jh = useLiveData ? liveJumpHosts : JUMPHOSTS
|
||||
const ex = useLiveData ? liveExitNodes : EXIT_NODES
|
||||
const jhSub = jh.length ? jh.map((j) => j.site).join(" · ") : "—"
|
||||
const exSub = ex.length ? ex.map((e) => e.site).join(" · ") : "—"
|
||||
const jhSub = <OptimizerSiteSummary kind="JumpHost" sites={useLiveData && !data ? undefined : jh.map(j=>j.site)} />
|
||||
const exSub = <OptimizerSiteSummary kind="Exit Node" sites={useLiveData && !data ? undefined : ex.map(e=>e.site)} />
|
||||
return [
|
||||
{
|
||||
label: "Home",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
|
||||
type OptimizerSiteSummaryProps = {
|
||||
kind: "JumpHost" | "Exit Node"
|
||||
sites?: readonly string[]
|
||||
}
|
||||
|
||||
export function OptimizerSiteSummary({kind,sites}:OptimizerSiteSummaryProps){
|
||||
const labels:string[]=[]
|
||||
if(sites===undefined)labels.push("Площадки неизвестны")
|
||||
else if(!sites.length)labels.push("Нет узлов в расчёте")
|
||||
else {
|
||||
const known=sites.map(site=>site.trim()).filter(site=>site && site!=="—")
|
||||
const distinct=[...new Set(known)]
|
||||
const missing=sites.length-known.length
|
||||
if(distinct.length){
|
||||
const names=distinct.length<=3 ? distinct.join(" · ") : ""
|
||||
labels.push(distinct.length<=3 && names.length<=24 ? names : `Площадки: ${distinct.length}`)
|
||||
}
|
||||
if(missing)labels.push(`Без площадки: ${missing}`)
|
||||
}
|
||||
return <div role="group" aria-label={`Площадки ${kind}`} className="flex min-w-0 max-w-full flex-wrap gap-1">
|
||||
{labels.map(label=><Badge key={label} variant="outline" size="sm" className="max-w-full whitespace-normal break-all text-left">{label}</Badge>)}
|
||||
</div>
|
||||
}
|
||||
@@ -181,3 +181,12 @@ Final1000WAN:134chars/86.5px высоты summary; WAN10:130chars/86.5px. В т
|
||||
Pure child CPU1000: equal1217.5→15.6ms,varied7967.5→14.5ms,freshness10с timeout→16.7ms; CPU20:11.5–11.7ms. Child startup отдельно~0.25s. Exact IDs проверены во всех6 профилях. Evidence docs/ux-ui/acceptance/optimizer-probes-cpu-2026-10-07.json содержит source SHA256, baseline HEAD и результаты. Браузерная live ветка7WAN/1000probes:219902bytes/7rows/222tableDOM/1018ms dev-ready; timestamps соответствуют p993..999, после refresh сохраняются,320px без overflow, API mutations отсутствуют. Это не production SLA/нагрузка PostgreSQL/RouterOS и не гарантия произвольного mixed pool: худшая экспоненциальная сложность DFS и full-route product остаются.
|
||||
|
||||
Следующий12.3.67: KPI captions JH/Exit (повторы/пустая площадка/unknown), затем separate full-route CPU profile. Общая main/context регрессия12.4.6/7 относится к прежним HEAD; этот delta принят профильными51+8. Внешняя12.5 не выполнена.
|
||||
|
||||
|
||||
## 12.3.67 — краткие и достоверные KPI-подписи площадок
|
||||
|
||||
2026-10-07. В существующем ReUI Stats12/KpiStatGrid footer переиспользован ReUI Badge outline/sm. Короткие уникальные названия (≤3 и≤24символов) показываются без повторов; большой/длинный список заменяется точным количеством площадок. Отдельно виден count узлов без площадки: пустые/whitespace/нормализованный — не считаются реальной площадкой. Нет полного join большого каталога. Неизвестный каталог сообщает «Площадки неизвестны»; нулевой topology pool — «Нет узлов в расчёте», потому что live topology исключает offline/disabled, а устройства могут существовать в inventory. Права/выбор узлов/расчёт/источники не менялись.
|
||||
|
||||
16 main browser +2 повторных новых профиля после copy-уточнения прошли:1000JH и1000Exit отдельно (без опасного cross product), repetitions/two sites/1000sites/long80chars/unknown/empty/offline/disabled/503 cache/recovery.320px без overflow, caption≤80px, scoped WCAG axe passed. Non-incremental TypeScript до dev, full page/helper/test lint и окончательный production build прошли. ReUI API/validate_usage проверены; primitive/theme не изменены. Historical9 optimizer PNG восстановлены, новые docs/ux-ui/screens/optimizer-sites-{jump-host,exit-node}-320.png просмотрены.
|
||||
|
||||
Далее12.3.68 pure child CPU profile full-route product≤100000/timeout10s/memory cap; не грузить тяжёлый baseline в UI. Остаточные DFS worst-case/full-route memory/native selector и внешняя12.5 не объявляются принятыми. Общая main/context регрессия относится к ранее записанным HEAD; текущий footer delta проверен профильными16+2.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.66 завершён: DFS score cache/one-time snapshot/lower-bound/child-bound, exact1230 fixtures,10pure+51main+8context/build passed. CPU1000 equal15.6ms,varied14.5ms,freshness16.7ms вместо10с timeout. Следующий12.3.67: краткие KPI captions JH/Exit с честными площадками/unknown. Затем отдельный CPU-профиль full-route product; внешняя12.5 остаётся.
|
||||
Актуальное продолжение 2026-10-07: 12.3.67 завершён: компактные ReUI KPI captions, distinct площадки/unknown/пустой и исключённый pool,16+2browser/type/lint/build. Следующий12.3.68: child CPU/full-route product≤100000, hard timeout/memory cap, сохранить baseline перед map lookup оптимизацией. Внешняя12.5 отдельно.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.66 завершён: DFS score cache/one-time snapshot/lower-bound/child-bound, exact1230 fixtures,10pure+51main+8context/build passed. CPU1000 equal15.6ms,varied14.5ms,freshness16.7ms вместо10с timeout. Следующий12.3.67: краткие KPI captions JH/Exit с честными площадками/unknown. Затем отдельный CPU-профиль full-route product; внешняя12.5 остаётся.
|
||||
Актуальное продолжение 2026-10-07: 12.3.67 завершён: компактные ReUI KPI captions, distinct площадки/unknown/пустой и исключённый pool,16+2browser/type/lint/build. Следующий12.3.68: child CPU/full-route product≤100000, hard timeout/memory cap, сохранить baseline перед map lookup оптимизацией. Внешняя12.5 отдельно.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
||||
|
||||
|
||||
@@ -758,3 +758,12 @@ Final1000WAN:134chars/86.5px высоты summary; WAN10:130chars/86.5px. В т
|
||||
Pure child CPU1000: equal1217.5→15.6ms,varied7967.5→14.5ms,freshness10с timeout→16.7ms; CPU20:11.5–11.7ms. Child startup отдельно~0.25s. Exact IDs проверены во всех6 профилях. Evidence docs/ux-ui/acceptance/optimizer-probes-cpu-2026-10-07.json содержит source SHA256, baseline HEAD и результаты. Браузерная live ветка7WAN/1000probes:219902bytes/7rows/222tableDOM/1018ms dev-ready; timestamps соответствуют p993..999, после refresh сохраняются,320px без overflow, API mutations отсутствуют. Это не production SLA/нагрузка PostgreSQL/RouterOS и не гарантия произвольного mixed pool: худшая экспоненциальная сложность DFS и full-route product остаются.
|
||||
|
||||
Следующий12.3.67: KPI captions JH/Exit (повторы/пустая площадка/unknown), затем separate full-route CPU profile. Общая main/context регрессия12.4.6/7 относится к прежним HEAD; этот delta принят профильными51+8. Внешняя12.5 не выполнена.
|
||||
|
||||
|
||||
## 12.3.67 — краткие и достоверные KPI-подписи площадок
|
||||
|
||||
2026-10-07. В существующем ReUI Stats12/KpiStatGrid footer переиспользован ReUI Badge outline/sm. Короткие уникальные названия (≤3 и≤24символов) показываются без повторов; большой/длинный список заменяется точным количеством площадок. Отдельно виден count узлов без площадки: пустые/whitespace/нормализованный — не считаются реальной площадкой. Нет полного join большого каталога. Неизвестный каталог сообщает «Площадки неизвестны»; нулевой topology pool — «Нет узлов в расчёте», потому что live topology исключает offline/disabled, а устройства могут существовать в inventory. Права/выбор узлов/расчёт/источники не менялись.
|
||||
|
||||
16 main browser +2 повторных новых профиля после copy-уточнения прошли:1000JH и1000Exit отдельно (без опасного cross product), repetitions/two sites/1000sites/long80chars/unknown/empty/offline/disabled/503 cache/recovery.320px без overflow, caption≤80px, scoped WCAG axe passed. Non-incremental TypeScript до dev, full page/helper/test lint и окончательный production build прошли. ReUI API/validate_usage проверены; primitive/theme не изменены. Historical9 optimizer PNG восстановлены, новые docs/ux-ui/screens/optimizer-sites-{jump-host,exit-node}-320.png просмотрены.
|
||||
|
||||
Далее12.3.68 pure child CPU profile full-route product≤100000/timeout10s/memory cap; не грузить тяжёлый baseline в UI. Остаточные DFS worst-case/full-route memory/native selector и внешняя12.5 не объявляются принятыми. Общая main/context регрессия относится к ранее записанным HEAD; текущий footer delta проверен профильными16+2.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,38 @@
|
||||
import {test,expect} from "@playwright/test"
|
||||
import axe from "axe-core"
|
||||
import {optimizerServersSchema} from "../../lib/route-optimizer-response"
|
||||
|
||||
for(const type of ["jump-host","exit-node"] as const)test(`Сводка оптимизатора1000: площадки ${type}`,async({page},info)=>{
|
||||
test.setTimeout(90000)
|
||||
const kind=type==="jump-host"?"JumpHost":"Exit Node",other=type==="jump-host"?"Exit Node":"JumpHost"
|
||||
const common={host:"192.0.2.1",enabled:true,status:"online",latency:10,country:"RU",model:"CHR"}
|
||||
let servers=optimizerServersSchema.parse([{...common,id:1,name:"Home",site:"MSK",type:"home-router",wanUplinks:[]},...Array.from({length:1000},(_,i)=>({...common,id:i+2,name:`Node ${i+1}`,site:i<600?"MSK":i<999?"NSK":" ",type}))])
|
||||
let failed=true,reads=0;const writes:string[]=[]
|
||||
await page.route("**/health",r=>r.fulfill({json:{ok:true}}))
|
||||
await page.route("**/api/**",r=>{
|
||||
const path=new URL(r.request().url()).pathname
|
||||
if(path==="/api/servers"){reads++;if(failed)return r.fulfill({status:503,json:{error:"Площадки не загружены"}})}
|
||||
if(r.request().method()!=="GET"&&!path.endsWith("/preview"))writes.push(path)
|
||||
return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/servers"?servers:path==="/api/filters/rules"?{rulesets:[]}:path==="/api/uptime/speed-probes"?{probes:[]}:path==="/api/ospf/catalog"?{interfaces:[],instances:[],neighbors:[],bfdSessions:[],routes:[],failures:[]}:path.endsWith("/ospf/optimize/preview")?{serverId:1,serverName:"Home",pingWeight:60,interfacesTotal:0,changedCount:0,unchangedCount:0,interfaces:[],changes:[]}: {}})
|
||||
})
|
||||
await page.goto("/route-optimizer?server=1")
|
||||
const root=page.locator('[aria-label="Сводка оптимизатора"]')
|
||||
const summary=()=>root.getByRole("group",{name:`Площадки ${kind}`,exact:true})
|
||||
await expect(summary()).toHaveText("Площадки неизвестны")
|
||||
failed=false;await page.getByRole("button",{name:"Обновить",exact:true}).click()
|
||||
await expect(summary()).toContainText("MSK · NSK");await expect(summary()).toContainText("Без площадки: 1")
|
||||
await expect(root.getByText("1000",{exact:true})).toBeVisible();await expect(root.getByRole("group",{name:`Площадки ${other}`,exact:true})).toHaveText("Нет узлов в расчёте")
|
||||
const refresh=async()=>{const before=reads;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect.poll(()=>reads).toBeGreaterThan(before)}
|
||||
servers=servers.map(row=>row.id===1?row:{...row,site:`SITE-${row.id}`});await refresh();await expect(summary()).toHaveText("Площадки: 1000")
|
||||
servers=servers.map(row=>row.id===1?row:{...row,site:"W".repeat(80)});await refresh();await expect(summary()).toHaveText("Площадки: 1")
|
||||
servers=servers.map(row=>row.id===1?row:{...row,site:""});await refresh();await expect(summary()).toHaveText("Без площадки: 1000")
|
||||
failed=true;await refresh();await expect(page.getByRole("alert").filter({hasText:"Последний полный снимок"})).toBeVisible();await expect(summary()).toHaveText("Без площадки: 1000")
|
||||
await page.setViewportSize({width:320,height:1000});expect(await page.evaluate(()=>document.documentElement.scrollWidth)).toBeLessThanOrEqual(320)
|
||||
const box=await summary().boundingBox();expect(box!.x).toBeGreaterThanOrEqual(0);expect(box!.x+box!.width).toBeLessThanOrEqual(320);expect(box!.height).toBeLessThan(80)
|
||||
await page.addStyleTag({content:"nextjs-portal{display:none!important}"});await root.screenshot({path:info.outputPath(`sites-${type}-320.png`),animations:"disabled"})
|
||||
await page.addScriptTag({content:axe.source});expect((await page.evaluate(async()=>(window as unknown as {axe:typeof axe}).axe.run(document.querySelector('[aria-label="Сводка оптимизатора"]')!,{runOnly:{type:"tag",values:["wcag2a","wcag2aa","wcag21a","wcag21aa"]}}))).violations.map(v=>({id:v.id,nodes:v.nodes.map(n=>n.target)}))).toEqual([])
|
||||
failed=false;servers=servers.map(row=>row.id===1?row:{...row,status:"offline" as const});await refresh();await expect(summary()).toHaveText("Нет узлов в расчёте")
|
||||
servers=servers.map(row=>row.id===1?row:{...row,status:"online" as const,enabled:false});await refresh();await expect(summary()).toHaveText("Нет узлов в расчёте")
|
||||
servers=servers.slice(0,1);await refresh();await expect(summary()).toHaveText("Нет узлов в расчёте")
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
Reference in new issue
Block a user