ADR-0056: Admin connectors KPI — on-demand SQL, без CQRS layer’а

Status: accepted Date: 2026-05-14 Deciders: belkanov, agent-claude

Plan указывал ADR-0051; номер 0051 уже занят (0051-public-api-auth-and-api-key-rbac.md). Phase 2 ADR получает следующий свободный номер 0056.

Контекст

Admin connectors page (/suppliers в web/admin) требует агрегаций ingestion_job_runs за 24 часа: top KPI tiles (коннекторов · циклов 24ч · ошибок · stale · expired · rate-limited), sidebar per-supplier metrics (account_count, runs, error_rate, lag, sparkline 24 hourly buckets), detail KPI (uptime, throughput, top error codes) и per-credential usage.

Объём данных за 24ч ≤ 1500 строк через VIEW admin_connector_runs_24h (migration 0132): ~24 buckets × ≤10 suppliers × ≤5 credentials × 3 status_class. Это тривиально для on-demand SQL запросов; admin page открывается оператором ≤10 раз в день, не RPS-нагрузка.

Cabinet Stats (cycle 2026-05-06, ADR не выделен) пошёл по пути pre-aggregated tables (cabinet_stats_minute/hour/day) + rollup worker. Это правильно для public-API observability, где per-tenant RPS-level нагрузка и многомесячные timeseries.

Решение

Admin connectors KPI — thin read-model без CQRS layer’а:

  • Один VIEW admin_connector_runs_24h (migration 0132) группирует runs за 24ч по supplier_ref/credential_id/bucket_hour/status_class/error_code. VIEW делает rename (suppliersupplier_ref, credential_refcredential_id) и derived status_class/error_code (см. Schema Errata в plan 2026-05-14-admin-connectors-phase2-analytics.md).
  • Repository connectorkpi/infra/postgres/repo.go через pgxpool.Pool напрямую. Нет агрегата, нет outbox event’ов, нет materialized state.
  • Handler backend/internal/platform/di/connectors_kpi.go синхронно запрашивает Service → Repository → SQL → DTO.
  • Кеш на стороне фронта: TanStack staleTime 30s, refetchInterval 60s.
  • AdminPathGate (role=admin JWT) — единственный auth check.

Альтернативы

A. Pre-aggregated tables (как cabinet_stats) — overkill для 24ч окна и ≤10 коннекторов. Rollup worker, миграции hour/day, eventual consistency, тестовая обвязка — все дорого для read pattern «оператор смотрит несколько раз в день».

B. Materialized VIEW + REFRESH — дополнительный operational toil (когда refresh? как реагировать на stale data?) без существенной выгоды при текущем объёме.

C. Prometheus + Grafana iframe в admin UI — потеря интеграции с UI (отдельная auth, отдельный layout), невозможность click-through из supplier row в /customers/{id}, требует отдельного pipeline export.

Последствия

  • Phase 2 admin KPI добавляет ровно: один VIEW (миграция 0132) + одну колонку (migration 0133 для stale_threshold_seconds) + пакет connectorkpi + handler в platform/di. Никаких новых background-процессов.
  • Если число коннекторов вырастет >50 или admin начнёт open’ить страницу minute-level (например при alert’ах) — пересматриваем в follow-up cycle (materialized VIEW + REFRESH, или partitioned table). Trigger criteria: p95 query latency > 500ms ИЛИ /suppliers RPS > 10.
  • Для public-API style observability (per-tenant analytics) ADR-0056 НЕ применяется — там pre-aggregated подход с rollup worker’ом остаётся authoritative.
  • Schedules-admin handler НЕ возвращает stale_threshold_seconds в payload (поле существует с migration 0133, но scheduleToAdminJSON не включает его). Phase 3 follow-up — расширить admin payload; до этого ConnectorRulesPanel показывает default 3600s.