diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index d2dc398b..89aadceb 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -279,6 +279,14 @@ function filterActiveInterfaces): SpeedProbeRow { + const active = filterActiveInterfaces(interfaces) + const unavailable = (name: string) => name !== "" && !active.some(item => item.name === name) + const srcInterface = draft.srcServerId === serverId && unavailable(draft.srcInterface) ? "" : draft.srcInterface + const dstInterface = draft.dstServerId === serverId && unavailable(draft.dstInterface) ? "" : draft.dstInterface + return srcInterface === draft.srcInterface && dstInterface === draft.dstInterface ? draft : { ...draft, srcInterface, dstInterface } +} + function serverMatchesSearch(server: Server, raw: string): boolean { const q = raw.trim().toLowerCase() if (!q) return true @@ -1291,37 +1299,24 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const loadSpeedInterfaces = useCallback(async (serverId: string) => { if (!liveApi) return - if (!serverId || speedIfaces[serverId]) return + if (!serverId) return + const cached = speedIfaces[serverId] + if (cached !== undefined) { + setSpeedDraft(previous => clearInactiveSpeedInterfaces(previous, serverId, cached)) + return + } const id = Number.parseInt(serverId, 10) if (!Number.isFinite(id)) return try { const res = await apiFetch<{ interfaces: Array<{ name: string; running: boolean; disabled: boolean; addresses: string[] }> }>(`/api/uptime/speed-test/endpoints/${id}/interfaces`) - setSpeedIfaces((prev) => ({ ...prev, [serverId]: res.interfaces ?? [] })) + const interfaces = res.interfaces ?? [] + setSpeedIfaces((prev) => ({ ...prev, [serverId]: interfaces })) + setSpeedDraft(previous => clearInactiveSpeedInterfaces(previous, serverId, interfaces)) } catch { setSpeedIfaces((prev) => ({ ...prev, [serverId]: [] })) + setSpeedDraft(previous => clearInactiveSpeedInterfaces(previous, serverId, [])) } - }, [apiFetch, liveApi, speedIfaces]) - - /** После загрузки списков интерфейсов сбросить выбор, если интерфейс не активен или отсутствует в списке */ - useEffect(() => { - if (!speedSheetOpen) return - setSpeedDraft((prev) => { - const rawSrc = speedIfaces[prev.srcServerId] - const rawDst = speedIfaces[prev.dstServerId] - let srcInterface = prev.srcInterface - let dstInterface = prev.dstInterface - if (rawSrc !== undefined) { - const active = filterActiveInterfaces(rawSrc) - if (srcInterface && !active.some((i) => i.name === srcInterface)) srcInterface = "" - } - if (rawDst !== undefined) { - const active = filterActiveInterfaces(rawDst) - if (dstInterface && !active.some((i) => i.name === dstInterface)) dstInterface = "" - } - if (srcInterface === prev.srcInterface && dstInterface === prev.dstInterface) return prev - return { ...prev, srcInterface, dstInterface } - }) - }, [speedSheetOpen, speedIfaces]) + }, [apiFetch, liveApi, speedIfaces, setSpeedDraft]) useEffect(() => { if (!sheetOpen) return diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 50f8427d..4c7d82c1 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. Следующий блок12.3.10:синхронизация speedDraft/формы/build, затем каталог устройств/schedule entries и остальные grids;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. Следующий блок12.3.11:ServerTileRail1000 устройств/поиск/URL/клавиатура/mobile Sheet, затем schedule entries и остальные grids;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 9c777644..f1c60f46 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -29,13 +29,21 @@ | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | -| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.9 приняты в описанных локальных границах | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.10 приняты в описанных локальных границах | +## 12.3.10 — синхронизация интерфейсов формы speed-проб (2026-10-04) + +Исходный set-state-in-effect устранён: недоступный интерфейс очищается при получении списка endpoint и при повторном открытии формы с кэшем. Проверка относится только к совпадающему src/dst serverId; активный интерфейс сохраняется. Effect с синхронным setSpeedDraft удалён, правило ESLint не отключалось. Права и подтверждение сохранения API сохранены. + +54 browser uptime links прошли, включая2 новых сценария active=false/true с холодной загрузкой и повторным открытием/проверкой PUT. Typecheck и полный ESLint изменённой страницы/тестов прошли без ошибок (6 исходных unused warnings). Повторный frontend production/contracts build прошёл:34 страницы; включает ResourceWarnings12.3.9. Живые интерфейсы RouterOS не проверены. + +Следующий блок12.3.11: ограничение DOM ServerTileRail на1000 устройствах с поиском по всему каталогу, выбранным сервером из URL, клавиатурой и мобильным Sheet. Затем оставшиеся25 grids: scheduler ограничен11 job keys, speed history backend limit200, остальные требуют проверки потребителя/API.12.4/12.5 не завершены. Коммит локальный, без push. + ## 12.3.9 — порционный список предупреждений ресурсов (2026-10-04) Список1000 серверов с CPU90% воспроизведён через обычный uptime overview:2009 warning nodes/4739 nodes страницы. ResourceWarnings применяет существующий шаблон AttentionRows:5 начальных записей,55 после раскрытия,105 после кнопки,5 после закрытия,55 при повторном раскрытии. Счётчик1000 остаётся полным, причины предупреждений и пороги не менялись. Начальная секция21 nodes, вся страница2751 nodes; таблица50 строк. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 23f29f77..67c61195 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -197,3 +197,11 @@ UptimeResourcesDataGrid использует ReUI client pagination по50 ст 9 browser ресурсов/URL/reload/320–1440px и отдельный large-case с WCAG-tagged axe audit предупреждений прошли; Enter/summary и порционное раскрытие проверены. Typecheck прошёл. Полный ESLint uptime page имеет исходную ошибку react-hooks/set-state-in-effect в setSpeedDraft после загрузки интерфейсов (подтверждена на HEAD2762f07),6 исходных unused warnings; с временным отключением этого правила остальные правила не дают ошибок. Это ограничение проверки, правило в файле не отключалось. Последний production build —12.3.8, после ResourceWarnings не повторялся. Следующий шаг12.3.10: устранить исходную ошибку синхронизации speedDraft с доступными интерфейсами, проверить формы и production build; затем ServerTileRail на1000 устройствах и оставшиеся25 grids. Демо GRE schedule хранится только в React state, live отдельного расписания нет:1000 entries пока не проверены.12.4/12.5 не завершены; локальная приёмка не доказывает реальные PG/RouterOS операции. Без push. + +## 12.3.10 — синхронизация интерфейсов формы speed-проб (2026-10-04) + +Исходный set-state-in-effect устранён: недоступный интерфейс очищается при получении списка endpoint и при повторном открытии формы с кэшем. Проверка относится только к совпадающему src/dst serverId; активный интерфейс сохраняется. Effect с синхронным setSpeedDraft удалён, правило ESLint не отключалось. Права и подтверждение сохранения API сохранены. + +54 browser uptime links прошли, включая2 новых сценария active=false/true с холодной загрузкой и повторным открытием/проверкой PUT. Typecheck и полный ESLint изменённой страницы/тестов прошли без ошибок (6 исходных unused warnings). Повторный frontend production/contracts build прошёл:34 страницы; включает ResourceWarnings12.3.9. Живые интерфейсы RouterOS не проверены. + +Следующий блок12.3.11: ограничение DOM ServerTileRail на1000 устройствах с поиском по всему каталогу, выбранным сервером из URL, клавиатурой и мобильным Sheet. Затем оставшиеся25 grids: scheduler ограничен11 job keys, speed history backend limit200, остальные требуют проверки потребителя/API.12.4/12.5 не завершены. Коммит локальный, без push. diff --git a/tests/e2e/uptime-links.spec.ts b/tests/e2e/uptime-links.spec.ts index e43c16c2..a945a952 100644 --- a/tests/e2e/uptime-links.spec.ts +++ b/tests/e2e/uptime-links.spec.ts @@ -663,3 +663,28 @@ test('Смена режима через настройки: поздняя ош await expect(page.getByText('Ошибка старого live-подключения', { exact: true })).toHaveCount(0) await expect(page.getByRole('button', { name: 'Новая проба', exact: true })).toBeEnabled() }) + +for (const active of [false, true]) test(`Speed interfaces: загрузка и повторное открытие, active=${active}`, async ({ page }) => { + await setup(page, "/uptime?tab=speed", 0, false, "mm:uptime:write") + const probe = { id: "s1", srcServerId: "1", dstServerId: "2", srcInterface: "ether1", dstInterface: "ether1", protocol: "tcp", direction: "both", durationSec: "10", enabled: true, lastRunAt: null, lastTxAvgMbps: null, lastRxAvgMbps: null, lastStatus: null, lastError: null, lastPingRttMs: null, lastPingLossPct: null, lastPingAt: null, lastPingError: null } + const bodies: { probes: Record[] }[] = [] + let interfaceCalls = 0 + await page.route("**/api/uptime/speed-probes", route => { + if (route.request().method() === "PUT") { bodies.push(route.request().postDataJSON()); return route.fulfill({ json: { ok: true } }) } + return route.fulfill({ json: { probes: [probe] } }) + }) + await page.route("**/api/uptime/speed-test/endpoints/*/interfaces", route => { interfaceCalls++; return route.fulfill({ json: { interfaces: [{ name: "ether1", running: active, disabled: !active, addresses: ["192.0.2.1/24"] }] } }) }) + await page.reload() + await page.getByRole("button", { name: "Пауза", exact: true }).click() + for (let attempt = 0; attempt < 2; attempt++) { + await page.getByRole("button", { name: "Редактировать speed-пробу", exact: true }).click() + await expect.poll(() => interfaceCalls).toBe(2) + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await dialog.getByRole("button", { name: "Сохранить", exact: true }).click() + await expect(dialog).toBeHidden() + expect(bodies[attempt].probes[0].srcInterface).toBe(active ? "ether1" : "") + expect(bodies[attempt].probes[0].dstInterface).toBe(active ? "ether1" : "") + } + expect(interfaceCalls).toBe(2) +})