fix(ui): согласовать настройки и контекст Community

This commit is contained in:
Denozordec committed 2026-10-01 01:04:02 +07:00
1 parent 6e6558bde9
commit 5a23dfcfe6
15 files changed
+33 -12

No files matched your search

+9 -8
View File
@@ -245,9 +245,9 @@ function ConfChip({ conf }: { conf: string }) {
return <Chip color={map[conf] ?? map.LOW}>{conf}</Chip>
}
function NInput({ value, onChange, min, max, label }: { label: string; value: number; onChange: (v: number) => void; min?: number; max?: number }) {
function NInput({ value, onChange, min, max, label, disabled }: { disabled?: boolean; label: string; value: number; onChange: (v: number) => void; min?: number; max?: number }) {
return (
<Input aria-label={label} type="number" value={value} min={min} max={max}
<Input disabled={disabled} aria-label={label} type="number" value={value} min={min} max={max}
onChange={e => onChange(Math.max(min ?? 0, Math.min(max ?? 9999, parseInt(e.target.value) || 0)))}
className="w-16 h-7 text-xs font-mono text-right" />
)
@@ -315,7 +315,7 @@ function HomeRouterCard({ entry, jumpHosts, pinned, applied, applying, onPin, on
<div className="ml-auto flex items-center gap-2">
{switchCount > 0 && (
<Chip color="bg-warning/10 text-warning border-warning/20">
{switchCount} переключений
Рекомендаций: {switchCount}
</Chip>
)}
{bestRoute && (
@@ -771,7 +771,7 @@ function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer
<SettingsIcon aria-hidden="true" className="size-4 text-muted-foreground shrink-0" />
<span className="text-sm font-medium flex-1">Настройки оптимизатора</span>
<span className="text-[11px] text-muted-foreground mr-2">
порог {settings.switchThreshold}% · ping {settings.pingWeight}% · зондирование {settings.probeIntervalMin} мин
порог {settings.switchThreshold} б. · ping {settings.pingWeight}% · обновление {settings.probeIntervalMin} мин
</span>
{showSettings ? <ChevronUpIcon aria-hidden="true" className="size-4 text-muted-foreground" /> : <ChevronDownIcon aria-hidden="true" className="size-4 text-muted-foreground" />}
</button>
@@ -781,12 +781,13 @@ function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="flex flex-col gap-3">
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">Пороги переключения</p>
<SettingRow label="Мин. выигрыш" unit="%">
<SettingRow label="Мин. выигрыш" unit="б.">
<NInput label="Порог переключения" value={settings.switchThreshold} onChange={v => set("switchThreshold", v)} min={0} max={50} />
</SettingRow>
<SettingRow label="Гистерезис" unit="%">
<NInput label="Порог гистерезиса" value={settings.hysteresisThreshold} onChange={v => set("hysteresisThreshold", v)} min={0} max={50} />
<SettingRow label="Гистерезис" unit="б.">
<NInput disabled label="Порог гистерезиса" value={settings.hysteresisThreshold} onChange={v => set("hysteresisThreshold", v)} min={0} max={50} />
</SettingRow>
<p className="text-[10px] text-muted-foreground">Гистерезис не участвует в этом расчёте.</p>
</div>
<div className="flex flex-col gap-3">
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">Веса метрик</p>
@@ -800,7 +801,7 @@ function RouteOptimizerContent({requestedServer,onSelectServer}:{requestedServer
<span>Ping {settings.pingWeight}%</span>
<span>BW {100 - settings.pingWeight}%</span>
</div>
<SettingRow label="Интервал зондирования">
<SettingRow label="Обновление расчёта">
<div className="flex rounded-md border border-input overflow-hidden h-7">
{[5, 10, 15, 30].map(v => (
<button type="button" aria-pressed={settings.probeIntervalMin===v} key={v} onClick={() => set("probeIntervalMin", v)}
+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.42; live WAN без моков/синтетики, далее подписи и локальная приёмка |
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.43; единицы/Community scope исправлены, далее финальная локальная приёмка |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 10.43 — смысл настроек и scope Community (2026-10-01)
- Порог выигрыша — баллы, не проценты; счётчик обозначает рекомендации, не выполненные переключения. Интервал обновляет расчёт, не запускает измерения. Неиспользуемый гистерезис недоступен и явно пояснён; значения сохранённых настроек совместимы.
- Live правила Community фильтруются по sourceServerId домашнего устройства до limit/ranking; правила разных home/exit не смешиваются.
- 4 observed unit, 10 E2E layout/tables/trust, TS/lint и ReUI consumer audit прошли. Девять снимков обновлены/просмотрены; форма 320 px теперь снята с начала панели. При просмотре заметен риск обрезки header badges/длинных имён — исправить до приёмки.
- Далее окончательный layout header/схема цепочки, общая optimizer regression и локальная приёмка.
## Блок 10.42 — WAN только из live каталога (2026-10-01)
- Live topology не ищет WAN по совпадению name/host/id в lib/data и не создаёт WAN1/1000 автоматически. Нет uplinks — нет цепочек/лучшего маршрута; пояснение ведёт к /servers?server=<id>. Для существующих WAN формула ранжирования прежняя; лимит модели явно назван расчётным.
+4
View File
@@ -220,3 +220,7 @@ Shared контракт и backend observed исходные значения/д
## 10.42. Live WAN без подстановки
Нет mock catalog/synthetic WAN в live topology; отсутствие uplinks объяснено с переходом к серверу. 6 unit, 16 E2E, TS/lint прошли. Далее подписи и Community scope перед визуальной приёмкой.
## 10.43. Настройки/Community scope
Порог в баллах, обновление расчёта, инертный гистерезис недоступен; рекомендации только своего home. 4 unit, 10 E2E, TS/lint прошли. Девять снимков обновлены; header clipping ещё исправить перед acceptance.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+1 -1
View File
@@ -518,7 +518,7 @@ export function buildLiveOptimizerData(
sorted.sort((a, b) => b.probabilityOptimal - a.probabilityOptimal)
const best = sorted[0] ?? null
const commRecs: CommRec[] = flatRules.slice(0, 24).map((rule) => {
const commRecs: CommRec[] = flatRules.filter(rule=>rule.sourceServerId===home.id).slice(0, 24).map((rule) => {
const curProb = 38 + (rule.community.length % 12)
const recProb = best ? Math.min(92, curProb + 18 + (best.score % 10)) : curProb
const rec = best
+8
View File
@@ -32,3 +32,11 @@ test("live topology never imports mock WAN or creates a synthetic uplink",()=>{
assert.equal(result.homes[0].bestRoute,null)
}
})
test("Community recommendations belong to their source home only",()=>{
const inventory=[...rows,{...rows[0],id:4,name:"Second home"}]
const rule=(id:string,community:string)=>({id,community,communityName:community,gateway:"192.0.2.2",gatewayTunnelId:"",description:""})
const result=buildLiveOptimizerData(inventory,[{serverId:"1",rules:[rule("a","65001:1")]},{serverId:"4",rules:[rule("b","65001:4")]},{serverId:"3",rules:[rule("c","65001:3")]}],DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS)
assert.deepEqual(result.homes[0].commRecs.map(r=>r.community),["65001:1"])
assert.deepEqual(result.homes[1].commRecs.map(r=>r.community),["65001:4"])
})
+3 -2
View File
@@ -15,7 +15,8 @@ for(const [device,width,height] of [["desktop",1440,1000],["mobile",390,844],["n
await settings.focus();await page.keyboard.press("Enter");await expect(settings).toHaveAttribute("aria-expanded","true")
await expect(page.getByRole("spinbutton",{name:"Вес задержки",exact:true})).toBeVisible()
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true)
await page.getByRole("spinbutton",{name:"Вес задержки",exact:true}).scrollIntoViewIfNeeded();
await expect(page.getByRole("spinbutton",{name:"Порог гистерезиса",exact:true})).toBeDisabled();
await settings.evaluate(element=>element.scrollIntoView({block:"start"}));
await page.screenshot({path:"docs/ux-ui/screens/route-optimizer-settings-"+device+".png",animations:"disabled"})
await settings.focus();await page.keyboard.press(" ");await expect(settings).toHaveAttribute("aria-expanded","false")
const slider=page.getByRole("slider",{name:"Максимальное число путей ECMP",exact:true});await expect(slider).toBeDisabled()
@@ -23,6 +24,6 @@ for(const [device,width,height] of [["desktop",1440,1000],["mobile",390,844],["n
const community=page.getByRole("button",{name:"BGP community (1)",exact:true});await community.focus();await page.keyboard.press("Enter");await expect(community).toHaveAttribute("aria-pressed","true")
const grid=page.getByRole("table").filter({hasText:"Community"});await expect(grid.locator("tr[data-row-id]")).toHaveCount(1)
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true)
await grid.scrollIntoViewIfNeeded();
await community.evaluate(element=>element.scrollIntoView({block:"start"}));
await page.screenshot({path:"docs/ux-ui/screens/route-optimizer-community-"+device+".png",animations:"disabled"})
})