@@ -1275,7 +1270,7 @@ export default function DataCollectionPage() {
id="run-filter"
className="h-8 rounded-md border border-input bg-background px-2 text-xs min-w-[180px]"
value={runFilterJobKey}
- onChange={(e) => setRunFilterJobKey(e.target.value)}
+ onChange={(e) => { setRunFilterJobKey(e.target.value); setOpenRunIds(new Set()) }}
>
{SCHEDULER_JOB_KEYS.map((k) => (
diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md
index c447c583..e8247c0a 100644
--- a/docs/ux-ui-plan.md
+++ b/docs/ux-ui-plan.md
@@ -226,7 +226,7 @@ REST запрос → ожидание → подтверждённый output /
| Пользователи | [Профиль](ux-ui/screens/users-profile-desktop.png), [ошибка](ux-ui/screens/users-error-desktop.png) | [390](ux-ui/screens/users-profile-mobile.png), [320](ux-ui/screens/users-profile-narrow.png) |
| Контейнеры | [Обзор](ux-ui/screens/containers-overview-desktop.png), [снимок](ux-ui/screens/containers-export-desktop.png) | [390](ux-ui/screens/containers-export-mobile.png), [320](ux-ui/screens/containers-export-narrow.png) |
-Актуальная точка продолжения: 11Б.3.3.1 — контекст и исходные lint ошибки страницы сбора; затем strict readers/dirty states. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал.
+Актуальная точка продолжения: 11Б.3.3.2 — strict scheduler runs и known/error/time журнала. Контекст/abort/revision и исходный lint исправлены в 11Б.3.3.1; далее независимые readers/dirty states и операции. GeoIP API принят в 11Б.3.2. Scheduler API основа проверена в 11Б.3.1, пока не подключена к странице. Совместимость JSONB/legacy snapshot и foreign guard проверены в 11Б.2; исходные lint/context ограничения страницы описаны в журнале. Общий runner исправлен в 11Б.1; карта оставшегося этапа — [11Б](ux-ui-stage-11b.md). Этап 11А принят локально в 11А.33: backups 71 основных + 28 source E2E, 27 frontend unit, backend профиль и production build. Backup scheduler cleanup проверен в 11А.32; общий dispatcher/journal status исправлен в 11Б.1, реальные интеграции остаются внешней приёмкой. S3/partial job/initial errors приняты в 11А.31.2.2. Диалоги ошибок приняты в 11А.31.2.1. Frame/первые 9 экранов приняты в 11А.31.1. URL/Filters контекст принят в 11А.30. Клиентский checkpoint restore принят в 11А.29.5. Backend outcome неизвестного импорта принят в 11А.29.4. Идентичность receipt принята в 11А.29.3. Backend cleanup удаления принят в 11А.29.2. Frontend delete/restore/download принят в 11А.29.1, затем URL/Frame/визуальная приёмка и scheduler cleanup. Frontend checkpoint/создание/serial polling приняты локально в 11А.28.2. API/основа наблюдения реализованы в 11А.28.1. Настройки расписания/S3 реализованы в 11А.27.1–11А.27.2. Каталоги/source/права реализованы в 11А.25, backend результат ручных операций исправлен в 11А.26. Сертификаты приняты локально в 11А.24, backups API стал строгим. State API, reader, snapshot merge и nullable представление подключены в 11А.14–11А.17.3; таблица требований — [этап 11А](ux-ui-stage-11a.md). Резервные копии и весь 11А приняты локально в 11А.33; внешние ограничения — журнал.
| Экран | ПК | Телефон |
| --- | --- | --- |
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md
index 58658f0e..a2e2ad0e 100644
--- a/docs/ux-ui-progress.md
+++ b/docs/ux-ui-progress.md
@@ -28,7 +28,7 @@
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
-| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.2; runner/JSONB/scheduler API/GeoIP проверены; далее контекст и состояния сбора |
+| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.3.1; runner/API/JSONB/GeoIP/гонки журнала проверены; далее состояния и формы сбора |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,14 @@
+## Блок 11Б.3.3.1 — контекст и гонки чтения общего журнала (2026-10-01)
+
+- Страница ждёт hydration источника; mode/backend задают ключ нового содержимого. При размонтировании читатель становится inactive, старые запросы отменяются и их callbacks не обновляют UI/не запускают последующее чтение.
+- Каждая загрузка отменяет предыдущую и имеет revision/30s deadline; все шесть GET используют общий AbortSignal. Поздний success/error после смены фильтра не заменяет актуальные строки/ошибки/busy. Promise.allSettled пока сохраняется: независимое появление быстрых зависимостей и dirty drafts ещё не реализованы.
+- Сброс раскрытых строк перенесён в обработчик фильтра; эффектные synchronous setState и ненужный stats useMemo устранены. ESLint страницы и нового теста прошёл без прежних 2 errors/2 warnings.
+- 2 новых filter-race + 6 GeoIP/snapshot E2E, production frontend/contracts build со встроенным TypeScript прошли. Отдельные E2E смены backend/mode и поздних мутаций ещё нужны в следующем блоке. Старые mutations не получили новой идемпотентности/lock; их приёмка не заявлена.
+- **Следующий блок 11Б.3.3.2:** подключение strict scheduler runs и known/error/time журнала; затем независимые settings/status readers, dirty drafts, действия/права и source E2E. GeoIP/NetFlow формы и вся 11Б ещё не приняты.
+
## Блок 11Б.3.2 — строгий GeoIP API против malformed crash (2026-10-01)
- Существующие GeoIP schemas подключены к GET/PUT/update; неполный status не попадает в UI state. Ответ проверяет ready/loaded и время, PUT подтверждает отправленные поля, update сверяет ok с partial/fatal/errors и счётчиками. False/null/zero не заменяются defaults, partial download остаётся фактом.
diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md
index 10e3a1d5..1087c368 100644
--- a/docs/ux-ui-stage-11b.md
+++ b/docs/ux-ui-stage-11b.md
@@ -63,3 +63,12 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
- 4 GeoIP API unit + 5 scheduler API unit, 6 E2E GeoIP/snapshot, профильный ESLint и production frontend/contracts build прошли. E2E fixture GeoIP приведена к полному существующему DTO. Первую проверку toast уточнили для двух dev StrictMode вызовов; повтор прошёл без pageerror.
- UI GeoIP/NetFlow пока сохраняет прежние defaults/dirty/context/mutation ограничения и требует отдельной приёмки; strict reader исправляет конкретное падение, не доказывает реальные загрузки mmdb или готовность интеграции.
- **Следующий блок 11Б.3.3.1:** изоляция контекста страницы сбора и исходные lint ошибки; затем подключение strict scheduler/settings readers, независимые состояния и черновики.
+
+
+## Блок 11Б.3.3.1 — контекст и гонки чтения общего журнала (2026-10-01)
+
+- Страница ждёт hydration источника; mode/backend задают ключ нового содержимого. При размонтировании читатель становится inactive, старые запросы отменяются и их callbacks не обновляют UI/не запускают последующее чтение.
+- Каждая загрузка отменяет предыдущую и имеет revision/30s deadline; все шесть GET используют общий AbortSignal. Поздний success/error после смены фильтра не заменяет актуальные строки/ошибки/busy. Promise.allSettled пока сохраняется: независимое появление быстрых зависимостей и dirty drafts ещё не реализованы.
+- Сброс раскрытых строк перенесён в обработчик фильтра; эффектные synchronous setState и ненужный stats useMemo устранены. ESLint страницы и нового теста прошёл без прежних 2 errors/2 warnings.
+- 2 новых filter-race + 6 GeoIP/snapshot E2E, production frontend/contracts build со встроенным TypeScript прошли. Отдельные E2E смены backend/mode и поздних мутаций ещё нужны в следующем блоке. Старые mutations не получили новой идемпотентности/lock; их приёмка не заявлена.
+- **Следующий блок 11Б.3.3.2:** подключение strict scheduler runs и known/error/time журнала; затем независимые settings/status readers, dirty drafts, действия/права и source E2E. GeoIP/NetFlow формы и вся 11Б ещё не приняты.
diff --git a/tests/e2e/data-collection-race.spec.ts b/tests/e2e/data-collection-race.spec.ts
new file mode 100644
index 00000000..8f8c274c
--- /dev/null
+++ b/tests/e2e/data-collection-race.spec.ts
@@ -0,0 +1,26 @@
+import { test, expect } from "@playwright/test"
+import { geoip } from "./fixtures/geoip"
+const row = { id: "current-run", jobKey: "backups", startedAt: "2026-10-01T00:00:00Z", finishedAt: "2026-10-01T00:00:00Z", status: "ok", error: null, durationMs: 100, resultJson: null }
+for (const fails of [true, false]) test("Сбор данных: поздний журнал после смены фильтра/" + fails, async ({ page }) => {
+ let started = false, completed = false, release!: () => void; const gate = new Promise(resolve => release = resolve)
+ const errors: string[] = []; page.on("pageerror", error => errors.push(error.message))
+ await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
+ await page.route("**/api/**", route => {
+ const path = new URL(route.request().url()).pathname
+ if (path === "/api/auth/config") return route.fulfill({ json: { required: false } })
+ if (path === "/api/geoip") return route.fulfill({ json: geoip })
+ if (path === "/api/traffic/flow/settings") return route.fulfill({ status: 503, json: { error: "NetFlow outside fixture" } })
+ return route.fulfill({ json: { enabled: false, intervalSec: 300, retentionDays: 14, lastCollectedAt: null, lastDurationMs: null, lastError: null, scheduler: { jobs: [] } } })
+ })
+ await page.route("**/api/scheduler/runs?**", async route => {
+ if (new URL(route.request().url()).searchParams.get("jobKey") === "backups") return route.fulfill({ json: { runs: [row] } })
+ started = true; await gate
+ try { await route.fulfill(fails ? { status: 503, json: { error: "СТАРАЯ ОШИБКА ЖУРНАЛА" } } : { json: { runs: [{ ...row, id: "old-run", error: "СТАРЫЙ РЕЗУЛЬТАТ ЖУРНАЛА" }] } }) } catch {} finally { completed = true }
+ })
+ await page.goto("/data-collection"); await expect.poll(() => started).toBe(true)
+ await page.getByLabel("Задача", { exact: true }).selectOption("backups")
+ await page.getByRole("button", { name: /Бэкапы.*100 мс/ }).click(); await expect(page.getByText("current-run", { exact: true })).toBeVisible()
+ release(); await expect.poll(() => completed).toBe(true)
+ await expect(page.getByText("current-run", { exact: true })).toBeVisible(); await expect(page.getByText(/СТАРАЯ ОШИБКА ЖУРНАЛА|СТАРЫЙ РЕЗУЛЬТАТ ЖУРНАЛА/)).toHaveCount(0)
+ expect(errors).toEqual([])
+})