diff --git a/app/(main)/data-collection/page.tsx b/app/(main)/data-collection/page.tsx index 53a05e55..4f0307bd 100644 --- a/app/(main)/data-collection/page.tsx +++ b/app/(main)/data-collection/page.tsx @@ -432,7 +432,11 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { function RunRowDetail({ r }: { r: SchedulerRunRowDto }) { const jobTitle = SCHEDULER_JOB_LABELS[r.jobKey] ?? r.jobKey const jobDesc = SCHEDULER_JOB_DESCRIPTIONS[r.jobKey] ?? "—" - const snapshot = useMemo(() => parseSchedulerRunSnapshot(r.resultJson ?? null), [r.resultJson]) + const snapshot = useMemo(() => { + const parsed = parseSchedulerRunSnapshot(r.resultJson) + return parsed?.job === r.jobKey ? parsed : null + }, [r.resultJson, r.jobKey]) + const hasSnapshot = r.resultJson != null && (typeof r.resultJson !== "string" || r.resultJson.trim() !== "") return (
Результаты измерений
{snapshot ? (
Снимок отсутствует (запись до обновления бекенда или задача без детализации).
diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md
index aebdfe72..2d22b081 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Б.2 — совместимость JSONB/legacy snapshot общего журнала. Общий 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 — строгие API и состояния сбора данных. Совместимость 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 5b0a69b5..9dc81590 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; общий runner исправлен, далее JSONB журнал |
+| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.2; runner/JSONB исправлены, далее строгие API и состояния сбора |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,15 @@
+## Блок 11Б.2 — JSONB и legacy snapshot общего журнала (2026-10-01)
+
+- resultJson читается как unknown: PostgreSQL JSONB объект и старый строковый JSON проходят общий decoder. Больше нет raw.trim на объекте. Проверены версия/известная задача/время/обязательные поля верхнего уровня; backup counts и новые diagnostics валидируются без подстановки нулей.
+- Снимок другого job не отображается в выбранной записи. Некорректный/неподдерживаемый снимок показывает Alert «Снимок недоступен», отсутствие снимка остаётся отдельным состоянием. Полная валидация вложенных строк остальных коллекций ещё относится к строгому API следующего блока.
+- 4 unit, 4 E2E object/legacy/invalid/foreign, TypeScript и production frontend/contracts build прошли. ESLint parser/DTO/tests прошёл. На общей странице остаются 2 исходные set-state-in-effect ошибки и 2 warnings: отдельный lint HEAD через stdin подтвердил ту же исходную базу; их исправление входит в reader/context блока 11Б.3.
+- Первый E2E прогон использовал неполную GeoIP фикстуру и падал в старом GeoipSettingsSection. Фикстура уточнена; NetFlow зависимость вынесена в явный HTTP 503 вместо неверного success DTO. Повторные 4 E2E прошли без pageerror. Строгий GeoIP reader/guard также необходим в 11Б, реальная интеграция не принята.
+- ReUI audit checklist получен; существующий Alert/вариант warning, текстовый рендер и структура сохранены. Весь раздел сбора данных ещё не прошёл визуальную/доступностную приёмку.
+- **Следующий блок 11Б.3:** строгие scheduler/collector API, независимые reader states, dirty drafts и контекст/права; новый run-now receipt пока не обрабатывается страницей строго.
+
## Блок 11Б.1 — общий scheduler runner и карта оставшихся разделов (2026-10-01)
- Collector вызывается один раз, результат записывается одной попыткой. Fatal/errors snapshot даёт status=error с сохранёнными счётчиками/снимком; partial отмечается текстом «Завершено с ограничениями». Наблюдаемое down/offline само по себе не является ошибкой выполнения коллектора. Skipped/not-due не порождают событие о выполненной работе.
diff --git a/docs/ux-ui-stage-11b.md b/docs/ux-ui-stage-11b.md
index 73eb6e27..dece60aa 100644
--- a/docs/ux-ui-stage-11b.md
+++ b/docs/ux-ui-stage-11b.md
@@ -35,3 +35,13 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
- 9 runner + 4 HTTP теста, backend/contracts build и ESLint изменённых файлов прошли. Общий npm --prefix backend test завершился с кодом 0; DB-зависимые тесты пропущены из-за недоступного PostgreSQL, это не проверка транзакций с реальной DB.
- Карта/референсы/схема оставшегося этапа сохранены в ux-ui-stage-11b.md. UI сбора данных пока не читает новый receipt строго; JSONB resultJson ошибочно ожидается строкой (raw.trim может выбросить ошибку). Следующий небольшой блок исправляет совместимость JSONB/legacy JSON; полные reader/state/операции — после него.
- **Следующий блок 11Б.2:** отображение object/legacy snapshot в общем журнале без падения, затем 11Б.3 строгие API и независимые состояния/черновики сбора данных. Этап 11Б/12 не принят, реальные DB/RouterOS/Telegram/Portal остаются внешней приёмкой.
+
+
+## Блок 11Б.2 — JSONB и legacy snapshot общего журнала (2026-10-01)
+
+- resultJson читается как unknown: PostgreSQL JSONB объект и старый строковый JSON проходят общий decoder. Больше нет raw.trim на объекте. Проверены версия/известная задача/время/обязательные поля верхнего уровня; backup counts и новые diagnostics валидируются без подстановки нулей.
+- Снимок другого job не отображается в выбранной записи. Некорректный/неподдерживаемый снимок показывает Alert «Снимок недоступен», отсутствие снимка остаётся отдельным состоянием. Полная валидация вложенных строк остальных коллекций ещё относится к строгому API следующего блока.
+- 4 unit, 4 E2E object/legacy/invalid/foreign, TypeScript и production frontend/contracts build прошли. ESLint parser/DTO/tests прошёл. На общей странице остаются 2 исходные set-state-in-effect ошибки и 2 warnings: отдельный lint HEAD через stdin подтвердил ту же исходную базу; их исправление входит в reader/context блока 11Б.3.
+- Первый E2E прогон использовал неполную GeoIP фикстуру и падал в старом GeoipSettingsSection. Фикстура уточнена; NetFlow зависимость вынесена в явный HTTP 503 вместо неверного success DTO. Повторные 4 E2E прошли без pageerror. Строгий GeoIP reader/guard также необходим в 11Б, реальная интеграция не принята.
+- ReUI audit checklist получен; существующий Alert/вариант warning, текстовый рендер и структура сохранены. Весь раздел сбора данных ещё не прошёл визуальную/доступностную приёмку.
+- **Следующий блок 11Б.3:** строгие scheduler/collector API, независимые reader states, dirty drafts и контекст/права; новый run-now receipt пока не обрабатывается страницей строго.
diff --git a/lib/scheduler-run-snapshot.test.ts b/lib/scheduler-run-snapshot.test.ts
new file mode 100644
index 00000000..c05535b0
--- /dev/null
+++ b/lib/scheduler-run-snapshot.test.ts
@@ -0,0 +1,22 @@
+import assert from "node:assert/strict"
+import { test } from "node:test"
+import { parseSchedulerRunSnapshot } from "./scheduler-run-snapshot"
+const snapshot = { v: 1, job: "backups", sampledAt: "2026-10-01T00:00:00Z", due: true, created: 2, failures: 0, pruned: 1, uploadFailures: 1, pruneFailures: 1, metadataError: "metadata unavailable", eventErrors: ["event unavailable"], errors: ["S3 unavailable"] }
+test("JSONB object and legacy JSON preserve the same snapshot and partial diagnostics", () => {
+ assert.deepEqual(parseSchedulerRunSnapshot(snapshot), snapshot)
+ assert.deepEqual(parseSchedulerRunSnapshot(JSON.stringify(snapshot)), snapshot)
+})
+test("missing, primitive, broken and future snapshots never throw or become valid empty data", () => {
+ for (const raw of [undefined, null, "", " ", "{", 1, false, [], "null", { ...snapshot, v: 2 }, { ...snapshot, job: "unknown" }, { ...snapshot, sampledAt: "bad" }]) assert.equal(parseSchedulerRunSnapshot(raw), null)
+})
+test("invalid backup counts, flags and diagnostic fields are rejected without zero substitution", () => {
+ for (const patch of [{ created: -1 }, { failures: "0" }, { pruned: 1.5 }, { created: undefined }, { due: undefined }, { skipped: "true" }, { errors: [{}] }, { eventErrors: [null] }, { metadataError: {} }, { uploadFailures: -1 }]) assert.equal(parseSchedulerRunSnapshot({ ...snapshot, ...patch }), null)
+})
+test("other collector envelopes support both representations and keep missing arrays unknown", () => {
+ for (const job of ["traffic", "uptime_resources", "servers_rest_ping", "uptime_ping", "uptime_speed"]) {
+ const key = job === "uptime_ping" ? "probes" : job === "uptime_speed" ? "runs" : "servers"
+ const value = { v: 1, job, sampledAt: snapshot.sampledAt, [key]: [] }
+ assert.deepEqual(parseSchedulerRunSnapshot(value), value); assert.deepEqual(parseSchedulerRunSnapshot(JSON.stringify(value)), value)
+ assert.equal(parseSchedulerRunSnapshot({ ...value, [key]: undefined }), null)
+ }
+})
diff --git a/lib/scheduler-run-snapshot.ts b/lib/scheduler-run-snapshot.ts
index 5197d058..ee7fc963 100644
--- a/lib/scheduler-run-snapshot.ts
+++ b/lib/scheduler-run-snapshot.ts
@@ -93,6 +93,10 @@ export interface BackupsRunSnapshot {
created: number
failures: number
pruned: number
+ uploadFailures?: number
+ pruneFailures?: number
+ metadataError?: string
+ eventErrors?: string[]
errors?: string[]
fatalError?: string
}
@@ -220,12 +224,32 @@ export interface SpeedScheduledRunSnapshot {
runs: SpeedRunSnapshot[]
}
-export function parseSchedulerRunSnapshot(raw: string | null | undefined): SchedulerRunSnapshot | null {
- if (raw == null || raw.trim() === "") return null
+/** PostgreSQL JSONB returns an object; older records may still contain JSON text. */
+export function parseSchedulerRunSnapshot(raw: unknown): SchedulerRunSnapshot | null {
try {
- const o = JSON.parse(raw) as { v?: unknown; job?: string }
- if (o?.v !== 1 || typeof o.job !== "string") return null
- return o as SchedulerRunSnapshot
+ const value: unknown = typeof raw === "string" ? (raw.trim() ? JSON.parse(raw) : null) : raw
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null
+ const o = value as Record