fix(ui): блокировать изменение проб во время Speed-test

This commit is contained in:
Denozordec committed 2026-09-21 21:14:13 +07:00
1 parent ce14f5de3e
commit cc40a5e33d
4 files changed
+46 -2

No files matched your search

+8
View File
@@ -1723,6 +1723,10 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const runSpeedTest = async (probe: SpeedProbeRow) => {
if (!requireWrite()) return
if (!probe.srcServerId || !probe.dstServerId || probe.srcServerId === probe.dstServerId) return
savingRef.current = true
setProbeSaving(true)
overviewReqRef.current++
const context = ++saveContext.current
setSpeedError(null)
setSpeedBusy(true)
const durationSec = Math.max(3, Number.parseInt(probe.durationSec, 10) || 10)
@@ -1771,6 +1775,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
method: "POST",
body: JSON.stringify(payload),
})
if (context !== saveContext.current) return
const ap = res.result.afterBtPing
const lines = (res.result.raw ?? []).flatMap((row) =>
Object.entries(row).map(([k, v]) => `${k}: ${String(v)}`),
@@ -1844,6 +1849,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
}) : p))
}
} catch (e) {
if (context !== saveContext.current) return
const message = e instanceof Error ? e.message : "Не удалось выполнить speed test"
setSpeedRuns((prev) => prev.map((r) => r.id === runId ? ({
...r,
@@ -1858,6 +1864,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
lastError: message,
}) : p))
} finally {
savingRef.current = false
setProbeSaving(false)
setSpeedBusy(false)
}
}
+8 -1
View File
@@ -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; переключатели подтверждаются API; удаление и звезда подтверждаются API; далее конкуренция измерений |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; удаление и звезда подтверждаются API; Speed-test блокирует запись; далее блокировка Ping |
| 8 | Трафик и статистика | Не начат |
| 9 | VPN, Firewall и Filters | Не начат |
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
@@ -29,6 +29,13 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 7.4Б3а — блокировка записи при Speed-test (2026-09-21)
- runSpeedTest занимает общий savingRef синхронно перед запросом: повторный запуск и CRUD проб блокируются, фоновые GET не перезаписывают измерение. В finally блокировка снимается при успехе/ошибке. Добавлена проверка saveContext перед применением позднего результата/ошибки после смены backend/mode или размонтирования.
- 6 профильных E2E прошли: блокировки форм, переключатели, звезда и новый сценарий одного Speed-test с запретом CRUD/Ping до ответа и освобождением после 500. Реальная смена backend/mode с отложенным Speed-ответом не проверялась отдельным E2E. Lint тестов чистый; uptime сохраняет 1 исходную ошибку и 6 предупреждений. ReUI checklist: композиция/компоненты не менялись. Production build и TypeScript прошли: 34 страницы.
- Следующий шаг 7.4Б3б: collect-now/collect-group должны занимать общую блокировку, исключать повторные запуски и поздние ответы. До этого уже запущенный Ping может пересекаться с записью/Speed. Также проверить ограничение времени ожидания Speed-test: сейчас общий gate удерживается до завершения fetch, отдельный timeout этой операции ещё не добавлен. Затем URL-контекст мониторинга и приёмка.
- На контрольной точке израсходовано 34% пятичасовой и 97% недельной квоты. Локальные HTTP-моки, без реального RouterOS/JWT. Коммит локальный без push.
## Блок 7.4Б2 — удаление и звезда (2026-09-21)
- После сброса пятичасовой квоты создан ранее отложенный коммит 4f23505. Предыдущий git add не выполнялся: auto-review не смог завершиться из-за лимита; повтор выполнен после восстановления квоты.
+1 -1
View File
@@ -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Б3 — конкуренция CRUD с измерениями Speed/Ping, повторные запуски и поздние ответы. Переключатели, удаление с подтверждением и звезда (7.4Б1–Б2) уже применяются после API через общий gate. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. CRUD строк подтверждается API; запуск измерений требует отдельной защиты от конкуренции. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии.
Следующий исполняемый шаг: блок 7.4Б3б — общая блокировка collect-now/collect-group, повторные запуски/поздние ответы и timeout Speed-test. Speed-test (7.4Б3а) уже занимает общий gate и блокирует CRUD до ответа. Переключатели, удаление с подтверждением и звезда (7.4Б1–Б2) уже применяются после API через общий gate. Ограничения прав и сохранение обеих форм после API выполнены; ошибки 400/403/500 и timeout сохраняют черновик. CRUD строк подтверждается API; запуск измерений требует отдельной защиты от конкуренции. Серверная часть 7.3А выполнена: can(mm:servers:write), ограничения кнопок, проверки обработчиков и 5 E2E прав. Codec URL серверов, hook и подключение к ServersPageContent выполнены. Полную регрессию всей системы здесь не повторять без изменений общей основы или найденной регрессии.
## Решение по синхронизации URL (7.1)
+29
View File
@@ -371,3 +371,32 @@ test("звезда дашборда: состояние меняется пос
await expect(page.getByRole("button", { name: "Убрать с дашборда", exact: true })).toHaveAttribute("aria-pressed", "true")
expect(calls).toBe(2)
})
test("Speed-test: один запуск, запись проб заблокирована до ответа и освобождается при ошибке", async ({ page }) => {
await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write")
await page.getByRole("button", { name: "Пауза", exact: true }).click()
await page.getByRole("button", { name: /Скорость/ }).click()
let calls = 0
let release!: () => void
const pending = new Promise<void>(resolve => { release = resolve })
await page.route("**/api/uptime/speed-test", async route => {
calls++
await pending
await route.fulfill({ status: 500, json: { error: "Измерение отклонено" } })
})
const run = page.getByRole("button", { name: "Запустить", exact: true })
await run.evaluate(button => { (button as HTMLButtonElement).click(); (button as HTMLButtonElement).click() })
await expect(run).toBeDisabled()
await expect(page.getByRole("button", { name: "Удалить speed-пробу", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Редактировать speed-пробу", exact: true })).toBeDisabled()
await expect(page.getByRole("switch")).toBeDisabled()
await expect.poll(() => calls).toBe(1)
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await expect(page.getByRole("button", { name: "Показать на дашборде", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Обновить", exact: true })).toBeDisabled()
release()
await expect(page.getByRole("button", { name: "Показать на дашборде", exact: true })).toBeEnabled()
await page.getByRole("button", { name: /Скорость/ }).click()
await expect(run).toBeEnabled()
await expect(page.getByText("Измерение отклонено", { exact: true }).first()).toBeVisible()
})