From 4f235058be01256136d1a868d32bf9663ca98fc9 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 21 Sep 2026 21:03:14 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B4=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B1=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/uptime/page.tsx | 37 +++++++++++++++++----------------- docs/ux-ui-progress.md | 10 ++++++++- docs/ux-ui-stage-7.md | 2 +- tests/e2e/uptime-links.spec.ts | 35 ++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index 2cf2dcf6..2f0d5b8e 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -1056,6 +1056,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId: string | null; allProbesHref: string }) { const { mode, backendUrl, backendStatus, checkBackend } = useDataSource() + const draftProbeId = useRef("") const [probeSaving, setProbeSaving] = useState(false) const [probeSaveError, setProbeSaveError] = useState(null) const savingRef = useRef(false) @@ -1579,11 +1580,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId // ── actions ── const toggleProbe = (id: string, v: boolean) => { if (!requireWrite()) return - setProbes((prev) => { - const next = prev.map((x) => x.id === id ? { ...x, enabled: v } : x) - void persistProbes(next).catch(error => setOpError(serverOperationError(error))) - return next - }) + const next = probes.map(p => p.id === id ? { ...p, enabled: v } : p) + void saveProbeForm(() => persistProbes(next), () => setProbes(next)) } const deleteProbe = (id: string) => { @@ -1637,6 +1635,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const openAddSheet = () => { if (!requireWrite()) return + draftProbeId.current = "p" + Date.now() setProbeSaveError(null) setEditingProbeId(null) setNewSrcId(selectableSources[0]?.id ?? "") @@ -1649,7 +1648,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId if (!requireWrite()) return if (!newName.trim() || !newTarget.trim() || !newSrcId) return const probeBase: PingProbe = { - id: editingProbeId ?? `p${Date.now()}`, + id: editingProbeId ?? draftProbeId.current, srcServerId: newSrcId, srcInterface: newSrcInterface, name: newName.trim(), @@ -1883,18 +1882,15 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const updateSpeedProbe = (id: string, patch: Partial) => { if (!requireWrite()) return - setSpeedProbes((prev) => { - const nextRows = prev.map((p) => { - if (p.id !== id) return p - const next = { ...p, ...patch } - if (patch.srcServerId && patch.srcServerId === next.dstServerId) { - next.dstServerId = selectableSources.find((s) => s.id !== patch.srcServerId)?.id ?? patch.srcServerId - } - return next - }) - void persistSpeedProbes(nextRows).catch(error => setSpeedError(serverOperationError(error))) - return nextRows + const nextRows = speedProbes.map(p => { + if (p.id !== id) return p + const next = { ...p, ...patch } + if (patch.srcServerId && patch.srcServerId === next.dstServerId) { + next.dstServerId = selectableSources.find(s => s.id !== patch.srcServerId)?.id ?? patch.srcServerId + } + return next }) + void saveProbeForm(() => persistSpeedProbes(nextRows), () => setSpeedProbes(nextRows)) } const openAddSpeedSheet = () => { @@ -1904,7 +1900,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId const src = selectableSources[0]?.id ?? "" const dst = selectableSources.find((s) => s.id !== src)?.id ?? src setSpeedDraft({ - id: "", + id: "sp-" + Date.now(), srcServerId: src, dstServerId: dst, srcInterface: "", @@ -1937,7 +1933,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId if (!speedDraft.srcServerId || !speedDraft.dstServerId) return const next = editingSpeedProbeId ? speedProbes.map(p => p.id === editingSpeedProbeId ? { ...p, ...speedDraft, id: editingSpeedProbeId, durationSec: String(speedDraft.durationSec ?? "10") } : p) - : [{ ...speedDraft, id: "sp-" + Date.now() }, ...speedProbes] + : [{ ...speedDraft, id: speedDraft.id }, ...speedProbes] void saveProbeForm(() => persistSpeedProbes(next), () => { setSpeedProbes(next) setEditingSpeedProbeId(null) @@ -2008,6 +2004,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {!canWrite && Только просмотрИзменение проб и запуск измерений требуют разрешения на запись. Обновление данных загружает сохранённые результаты.} {permissionError && !sheetOpen && !speedSheetOpen && {permissionError}} + {probeSaveError && !sheetOpen && !speedSheetOpen && {probeSaveError}} + {/* ── Live indicator ── */} {!isPaused && (
{ if (!requireWrite()) return setProbeSaveError(null) + draftProbeId.current = "p" + Date.now() setEditingProbeId(null) setNewSrcId(srv.id) setNewSrcInterface("") diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 1641fa3b..528105dd 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -21,7 +21,7 @@ | 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже | | 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже | | 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже | -| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; далее действия строк | +| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; далее удаление и звезда | | 8 | Трафик и статистика | Не начат | | 9 | VPN, Firewall и Filters | Не начат | | 10 | Маршрутизация, диагностика, карта и терминал | Не начат | @@ -29,6 +29,14 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 7.4Б1 — подтверждённые переключатели (2026-09-21) + +- По прямому поручению пользователя работа продолжена ниже резерва. Ping enabled и Speed update теперь используют общий saveProbeForm: синхронная блокировка, timeout, защита от устаревшего чтения, применение строки только после PUT. Ошибка видна вне Sheet и не сбрасывается фоновым GET. +- ID новой пробы создаётся при открытии формы и сохраняется при повторной отправке. Установленные компоненты ReUI и тема сохранены; checklist проверен. +- 14 профильных E2E прошли суммарно: 12 сценариев форм/прав, затем 2 сценария переключения (ожидание, 403, повтор). Первоначальные два сбоя были неоднозначным селектором alert из-за Next route-announcer; селектор уточнён. ESLint: прежние 1 ошибка/6 предупреждений uptime, тесты чистые. Финальный production build прошёл: 34 страницы, TypeScript без ошибок. +- Осталось: подтверждение удаления, применение удаления только после API, ошибка звезды и конкуренция ранее запущенных PATCH/Speed-test/PUT. Эти действия пока не переведены на общую блокировку. Локальные HTTP-моки не проверяют RouterOS/JWT. +- Квота на завершении блока: 96% пятичасовой и 92% недельной израсходовано. Следующий шаг — 7.4Б2 (удаление и звезда), затем URL мониторинга и приёмка. Коммит локальный, без push. + ## Блок 7.3В/7.4А — подтверждённое сохранение форм (2026-09-21) - Ping/Speed: формы ожидают PUT, обновляют локальный список и закрываются только после успешного ответа. При 400/403/500, сетевой ошибке или таймауте сохраняются поля и показывается ошибка в Sheet. Общий serverOperationError переиспользован для понятного 403 и неопределённого результата timeout. diff --git a/docs/ux-ui-stage-7.md b/docs/ux-ui-stage-7.md index 42058701..6f46b216 100644 --- a/docs/ux-ui-stage-7.md +++ b/docs/ux-ui-stage-7.md @@ -61,7 +61,7 @@ URL — источник сохранённого состояния. Back/Forwa 3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие. 4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении. -Следующий исполняемый шаг: блок 7.4Б — подтверждение удаления и достоверность действий строк Ping/Speed, включая звезду и конкурирующие записи. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. Оптимистичные row-handlers ещё требуют доработки. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии. +Следующий исполняемый шаг: блок 7.4Б2 — подтверждение удаления, звезда и конкурирующие записи Ping/Speed. Переключатели (7.4Б1) уже применяются только после API. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. Оптимистичные row-handlers ещё требуют доработки. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии. ## Решение по синхронизации URL (7.1) diff --git a/tests/e2e/uptime-links.spec.ts b/tests/e2e/uptime-links.spec.ts index f26e8f0d..811ebbc5 100644 --- a/tests/e2e/uptime-links.spec.ts +++ b/tests/e2e/uptime-links.spec.ts @@ -268,3 +268,38 @@ for (const speed of [false, true]) { await expect(dialog).toBeVisible() }) } + +for (const speed of [false, true]) { + test(`переключение ${speed ? "Speed" : "Ping"}: ожидание API, отказ, повтор без потери состояния`, async ({ page }) => { + await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write") + await page.getByRole("button", { name: "Пауза", exact: true }).click() + if (speed) await page.getByRole("button", { name: /Скорость/ }).click() + let calls = 0 + let fail = true + let release!: () => void + const pending = new Promise(resolve => { release = resolve }) + await page.route(speed ? "**/api/uptime/speed-probes" : "**/api/uptime/probes", async route => { + if (route.request().method() !== "PUT") return route.fallback() + calls++ + expect(route.request().postDataJSON().probes[0].enabled).toBe(false) + if (fail) await pending + await route.fulfill({ status: fail ? 403 : 200, json: fail ? { error: "Forbidden" } : { ok: true } }) + }) + const toggle = page.getByRole("switch") + await expect(toggle).toBeChecked() + await toggle.click() + await expect(toggle).toBeDisabled() + await expect(toggle).toBeChecked() + await expect.poll(() => calls).toBe(1) + release() + await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toBeVisible() + await expect(toggle).toBeChecked() + await expect(toggle).toBeEnabled() + fail = false + await toggle.click() + await expect(toggle).not.toBeChecked() + await expect(toggle).toBeEnabled() + await expect(page.getByRole("alert").filter({ hasText: "Недостаточно прав" })).toHaveCount(0) + expect(calls).toBe(2) + }) +}