diff --git a/app/(main)/probes/page.tsx b/app/(main)/probes/page.tsx index 4809d8c8..3a2101cb 100644 --- a/app/(main)/probes/page.tsx +++ b/app/(main)/probes/page.tsx @@ -590,7 +590,7 @@ function ProbesContent() { const [traceUseDns, setTraceUseDns] = useState(false) const [traceHops] = useState(30) const [dnsType, setDnsType] = useState("A") - const [bwTunId, setBwTunId] = useState("") + const [bwChoice, setBwChoice] = useState<{ serverId: string; id: string } | null>(null) const [bwProto, setBwProto] = useState<"tcp" | "udp">("tcp") const [bwDuration, setBwDuration] = useState(10) const [runBusy, setRunBusy] = useState(false) @@ -671,11 +671,6 @@ function ProbesContent() { }, [isLive, greByServer, srcId]) - useEffect(() => { - const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId) - if (list.length && !list.some(t => t.id === bwTunId)) setBwTunId(list[0]!.id) - }, [isLive, greByServer, srcId, bwTunId]) - // ── run state ── const [tests, setTests] = useState([]) const [tab, setTab] = useState<"history" | "schedule">("history") @@ -694,6 +689,8 @@ function ProbesContent() { }, []) const bwTunnels = tunnelsForSrc + const bwTunId = bwChoice?.serverId === srcId ? bwChoice.id : (bwTunnels[0]?.id ?? "") + const setBwTunId = (id: string) => setBwChoice({ serverId: srcId, id }) const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId]) const sourceKey = JSON.stringify([srcId, srcServer?.host]) const sourceRequired = tool === "ping" || tool === "traceroute" || tool === "mtu" @@ -744,7 +741,7 @@ function ProbesContent() { target: tool === "bandwidth" ? (bwTun?.name ?? target) : target, command: cmdPreview, startedAt: Date.now(), - lines: [{ text: "Выполняется запрос к MikroTik через API…", kind: "dim" }], + lines: [{ text: tool === "dns" ? "Выполняется DNS-запрос приложения…" : "Выполняется запрос к MikroTik через API…", kind: "dim" }], totalLines: 1, source: "live", }, ...p.slice(0, 9)]) @@ -1014,7 +1011,8 @@ function ProbesContent() { {tool === "bandwidth" && (
GRE-туннель (цель) - + t.id === bwTunId) ? bwTunId : ""} onChange={setBwTunId} className="min-w-[180px]"> + {!bwTunnels.some(t => t.id === bwTunId) && } {bwTunnels.map(t => )}
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 9d81599a..7db57822 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -26,7 +26,7 @@ | 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.69; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе | +| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.70; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | @@ -36,6 +36,13 @@ +## Блок 10.70 — контекст BW и клавиатурные действия (2026-10-01) + +- Выбор GRE для BW вычисляется из текущего каталога без setState в effect. Первоначальный выбор сохраняет прежнее поведение; ручной выбор привязан к устройству. Если выбранный GRE исчез, запуск блокируется до нового явного выбора, другой туннель не подставляется. +- Ожидание DNS теперь подписано как DNS-запрос приложения. Клавиатурой проверены DNS/CNAME, Enter из поля цели, копирование результата с подтверждением и удаление с возвратом фокуса. +- TypeScript и lint тестов прошли. Профильный прогон: 42 сценария passed; один новый тест сначала упал из-за неполной GRE HTTP-фикстуры, после исправления контракта и aria селектора прошёл отдельным повтором. Производственный код не обходил проблему теста. Прежняя диагностическая lint ошибка устранена; три оставшиеся относятся к расписанию 11Б. +- Далее 10.71: итоговая профильная регрессия этапа 10, production build, перечень внешних ограничений и локальная приёмка. Затем 11А: пользователи. Моки не доказывают реальную сеть/БД/портал. + ## Блок 10.69 — доступность и адаптивность диагностики (2026-10-01) - Сохранена композиция Frame/FramePanel, OpsPanel и ServerRail; API Frame и ReUI checklist сверены. Vendor/theme не изменены. Цвета диагностики используют семантические токены, анимация учитывает reduced motion, кнопки/переключатели и область вывода имеют доступные имена и состояния. diff --git a/docs/ux-ui-stage-10.md b/docs/ux-ui-stage-10.md index 7438b188..470de252 100644 --- a/docs/ux-ui-stage-10.md +++ b/docs/ux-ui-stage-10.md @@ -442,3 +442,11 @@ Shared контракт и backend observed исходные значения/д - На 1440/390/320 px нет горизонтального переполнения; команда на телефоне использует всю доступную ширину, адрес цели читается, DNS подписан как тест приложения. Сохранены и визуально проверены шесть снимков ready/output. - TypeScript, 37 data/source E2E, 4 layout/clipboard E2E и 4 context E2E прошли. Production frontend build прошёл. Lint изменённого теста чистый; в странице остаются четыре прежние set-state-in-effect ошибки (три в расписании, одна в выборе BW). Новых ошибок/предупреждений нет. - Далее 10.70: убрать эффект выбора BW в диагностике, дополнить проверку клавиатуры/успешного clipboard и локальную приёмку. Расписание и три его lint ошибки перенесены в 11Б; реальные RouterOS/БД/портал не приняты по мокам. + + +## Блок 10.70 — контекст BW и клавиатурные действия (2026-10-01) + +- Выбор GRE для BW вычисляется из текущего каталога без setState в effect. Первоначальный выбор сохраняет прежнее поведение; ручной выбор привязан к устройству. Если выбранный GRE исчез, запуск блокируется до нового явного выбора, другой туннель не подставляется. +- Ожидание DNS теперь подписано как DNS-запрос приложения. Клавиатурой проверены DNS/CNAME, Enter из поля цели, копирование результата с подтверждением и удаление с возвратом фокуса. +- TypeScript и lint тестов прошли. Профильный прогон: 42 сценария passed; один новый тест сначала упал из-за неполной GRE HTTP-фикстуры, после исправления контракта и aria селектора прошёл отдельным повтором. Производственный код не обходил проблему теста. Прежняя диагностическая lint ошибка устранена; три оставшиеся относятся к расписанию 11Б. +- Далее 10.71: итоговая профильная регрессия этапа 10, production build, перечень внешних ограничений и локальная приёмка. Затем 11А: пользователи. Моки не доказывают реальную сеть/БД/портал. diff --git a/docs/ux-ui/screens/probes-output-desktop.png b/docs/ux-ui/screens/probes-output-desktop.png index 0e04eaba..3cae7b6f 100644 Binary files a/docs/ux-ui/screens/probes-output-desktop.png and b/docs/ux-ui/screens/probes-output-desktop.png differ diff --git a/docs/ux-ui/screens/probes-output-mobile.png b/docs/ux-ui/screens/probes-output-mobile.png index 7801c04f..ccbde747 100644 Binary files a/docs/ux-ui/screens/probes-output-mobile.png and b/docs/ux-ui/screens/probes-output-mobile.png differ diff --git a/docs/ux-ui/screens/probes-output-narrow.png b/docs/ux-ui/screens/probes-output-narrow.png index c9818675..ebb85655 100644 Binary files a/docs/ux-ui/screens/probes-output-narrow.png and b/docs/ux-ui/screens/probes-output-narrow.png differ diff --git a/docs/ux-ui/screens/probes-ready-desktop.png b/docs/ux-ui/screens/probes-ready-desktop.png index 0b70e358..ae8de5d3 100644 Binary files a/docs/ux-ui/screens/probes-ready-desktop.png and b/docs/ux-ui/screens/probes-ready-desktop.png differ diff --git a/docs/ux-ui/screens/probes-ready-mobile.png b/docs/ux-ui/screens/probes-ready-mobile.png index 71f5a1b9..66fda32d 100644 Binary files a/docs/ux-ui/screens/probes-ready-mobile.png and b/docs/ux-ui/screens/probes-ready-mobile.png differ diff --git a/docs/ux-ui/screens/probes-ready-narrow.png b/docs/ux-ui/screens/probes-ready-narrow.png index e132d95b..18da04f0 100644 Binary files a/docs/ux-ui/screens/probes-ready-narrow.png and b/docs/ux-ui/screens/probes-ready-narrow.png differ diff --git a/tests/e2e/probes-data.spec.ts b/tests/e2e/probes-data.spec.ts index a44c0d33..e4247975 100644 --- a/tests/e2e/probes-data.spec.ts +++ b/tests/e2e/probes-data.spec.ts @@ -112,3 +112,12 @@ test("Диагностика: URL выбор, назад и reload сохран await page.goto("/probes?server=2&keep=1#result");await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true") await page.getByRole("option",{name:/Первый/}).click();await expect(page).toHaveURL(/keep=1&server=1#result$/);await page.goBack();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true");await page.reload();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true") }) + +test("Диагностика: исчезнувший выбранный GRE не подменяется первым",async({page})=>{ + let tunnels=[{...tunnel,id:"gre-a",name:"Первый GRE"},{...tunnel,id:"gre-b",name:"Выбранный GRE",remoteAddress:"192.0.2.3"}] + await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill({json:{tunnels}})) + await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();const choice=page.getByRole("combobox",{name:"GRE-туннель для BW-теста"});await expect(run(page)).toBeEnabled();await choice.selectOption("gre-b") + tunnels=tunnels.slice(0,1);await page.getByRole("button",{name:"Обновить устройства диагностики",exact:true}).click() + await expect(choice.getByRole("option",{name:"Выбранный GRE"})).toHaveCount(0);await expect(choice).toHaveValue("");await expect(run(page)).toBeDisabled() + await choice.selectOption("gre-a");await expect(run(page)).toBeEnabled() +}) diff --git a/tests/e2e/probes-layout.spec.ts b/tests/e2e/probes-layout.spec.ts index a37f9e89..9dec142d 100644 --- a/tests/e2e/probes-layout.spec.ts +++ b/tests/e2e/probes-layout.spec.ts @@ -18,3 +18,13 @@ test("Диагностика: недоступный clipboard сохраняе await page.addInitScript(()=>Object.defineProperty(navigator,"clipboard",{value:{writeText:()=>Promise.reject(new Error("permission denied"))},configurable:true})) await page.goto("/probes?server=1");await page.getByRole("button",{name:"Скопировать параметры диагностики"}).click();await expect(page.getByText(/Не удалось скопировать/)).toBeVisible();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("8.8.8.8") }) + +test("Диагностика: клавиатура, подтверждённый clipboard и место DNS",async({page})=>{ + await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>r.fulfill({json:new URL(r.request().url()).pathname==="/api/servers"?[{id:1,name:"Устройство",host:"192.0.2.1",enabled:true,status:null,type:"exit-node"}]:new URL(r.request().url()).pathname==="/api/auth/config"?{required:false}:{tunnels:[]}})) + await page.addInitScript(()=>Object.defineProperty(navigator,"clipboard",{value:{writeText:async(text:string)=>{sessionStorage.setItem("test-clipboard",text)}},configurable:true})) + await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:"example.test. IN CNAME target.test."}})) + await page.goto("/probes?server=1");const dns=page.getByRole("button",{name:"DNS",exact:true});await dns.focus();await page.keyboard.press("Enter");const cname=page.getByRole("button",{name:"CNAME",exact:true});await cname.focus();await page.keyboard.press("Space");await expect(cname).toHaveAttribute("aria-pressed","true") + const target=page.getByRole("textbox",{name:"Домен"});await target.fill("example.test");await target.press("Enter");const region=page.getByRole("region",{name:/^Вывод DNS/});await expect(region).toContainText("example.test. IN CNAME target.test.");await expect(target).toBeFocused();await expect(page.getByText("DNS приложения",{exact:true})).toBeVisible() + const copy=page.getByRole("button",{name:/^Скопировать показанный вывод/});await copy.focus();await page.keyboard.press("Enter");await expect(page.getByText("Скопировано.",{exact:true})).toBeVisible();expect(await page.evaluate(()=>sessionStorage.getItem("test-clipboard"))).toBe("example.test. IN CNAME target.test.") + const remove=page.getByRole("button",{name:/^Удалить результат/});await remove.focus();await page.keyboard.press("Enter");await expect(region).toHaveCount(0);await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeFocused() +})