Files
Deal/docs/architecture/2026-09-10-operator-analytics-contract.md
T
2026-09-13 19:18:16 +03:00

14 KiB

Дейл — контракт операторской аналитики и аудита действий (этап 10, T1–T3)

Дата: 2026-09-10 Статус: контракт для фронта (оператор-консоль, T4). Источник истины для src/frontend. Связанные документы: docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md, docs/architecture/2026-09-10-unified-api-contract.md.

Общие правила

  • Только операторская сессия. Все ручки /api/operator/* (включая аналитику) требуют разрешённой операторской сессии (кука deal_operator_session). Без неё — 401 { "detail": "Требуется вход оператора" }.
  • Все ответы — JSON camelCase.
  • Время на wire в этих ручках — ISO-8601 (DateTimeOffset, UTC, напр. 2026-09-10T15:22:46.123Z). Query-параметры from/to и поля at/from/to используют ISO-8601 (как уже принято в GET /api/operator/audit). Ключи агрегатов groupBy=day — строки ГГГГ-ММ-ДД.
  • Диапазоны from/toвключительно; не заданы — без границы.
  • Ошибки — { "detail": "текст" }. Коды: 400 (некорректный ввод, напр. неизвестный groupBy), 401 (нет операторской сессии).
  • Все ручки аналитики — read-only (ничего не меняют).

Каталог событий аудита (для фильтров eventType / ленты действий)

К SaaS-событиям этапа 7 добавлены (этап 10, T1; актор tenant — действия пользователя тенанта):

eventType Когда
tenant_logout выход пользователя тенанта (POST /api/auth/logout)
operator_logout выход оператора (POST /api/operator/auth/logout)
invite_joined активация инвайта (POST /api/join)
card_created создание карточки
card_moved перенос карточки между контейнерами
card_trashed карточка отправлена в корзину
card_restored карточка возвращена из корзины/архива
card_deleted карточка удалена навсегда
card_comment_added добавлен комментарий к карточке
container_created создан контейнер/колонка
container_updated изменён контейнер/колонка
container_deleted удалён контейнер/колонка
settings_updated сохранены настройки тенанта
channel_enabled включён мониторинг канала Telegram
channel_created канал добавлен в каталог (резерв каталога)
telegram_linked аккаунт Telegram привязан (фаза ready)
telegram_keys_changed оператор изменил глобальные ключи Telegram (PUT /api/operator/settings/telegram-keys)

Типы акторов (actorType): tenant, operator, system. Секреты (пароли, токены, api-ключи) в detailJson не пишутся.


GET /api/operator/analytics/overview

Сводка за период: тенанты, расход токенов, события, входы/выходы/неудачные входы.

Query

Параметр Тип Обяз. Описание
from ISO-8601 нет начало периода (включительно)
to ISO-8601 нет конец периода (включительно)

200

{
  "tenantsTotal": 12,
  "tenantsActive": 10,
  "promptTokens": 1250000,
  "completionTokens": 320000,
  "totalTokens": 1570000,
  "tokenEvents": 842,
  "events": 5012,
  "logins": 320,
  "logouts": 288,
  "failedLogins": 17,
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-10-01T00:00:00Z"
}
  • tokens*/tokenEvents — сумма по событиям public.token_usage_events за период.
  • events — число записей аудита за период.
  • logins = tenant_login_ok + operator_login_ok; logouts = tenant_logout + operator_logout; failedLogins = tenant_login_failed + operator_login_failed.

Коды: 200, 401.


GET /api/operator/analytics/tokens

Серия/агрегаты расхода токенов.

Query

Параметр Тип Обяз. Описание
groupBy enum нет day (дефолт) | tenant | provider | model
tenantId uuid нет фильтр по тенанту
from ISO-8601 нет начало периода (включительно)
to ISO-8601 нет конец периода (включительно)

200

{
  "groupBy": "day",
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-10-01T00:00:00Z",
  "items": [
    { "key": "2026-09-10", "promptTokens": 1200, "completionTokens": 300, "totalTokens": 1500, "eventCount": 42 }
  ],
  "total": { "key": "total", "promptTokens": 1250000, "completionTokens": 320000, "totalTokens": 1570000, "eventCount": 842 }
}
  • key группы: dayГГГГ-ММ-ДД (сутки UTC); tenant — Guid D; provider — id провайдера (deepseek/openai/…, для ML — local); model — модель (ml для локальной ML-модели).
  • Порядок items: day — по возрастанию даты; tenant/provider/model — по убыванию totalTokens.
  • total — итог по всем строкам.

Коды: 200; 400 { "detail": "Неизвестная группировка (day|tenant|provider|model)" }; 401.


GET /api/operator/analytics/activity

Лента действий (аудит) с фильтрами и пагинацией.

Query

Параметр Тип Обяз. Описание
eventType string нет тип события (см. каталог)
actorType enum нет tenant | operator | system
actorId uuid нет идентификатор актора
tenantId uuid нет тенант
from ISO-8601 нет нижняя граница at (включительно)
to ISO-8601 нет верхняя граница at (включительно)
limit int нет размер страницы (дефолт 100, кламп 1..500)
offset int нет смещение (≥0)

200

{
  "items": [
    {
      "eventType": "card_moved",
      "actorType": "tenant",
      "actorId": "1f2e3d4c-5b6a-7980-1234-56789abcdef0",
      "tenantId": "aabbccdd-eeff-0011-2233-445566778899",
      "tenantName": "ООО «Ромашка»",
      "ip": "203.0.113.7",
      "changes": [
        { "field": "cardId", "to": "c_1a2b3c4d5e6f" },
        { "field": "to", "to": "planned" }
      ],
      "detailJson": "{\"changes\":[{\"field\":\"cardId\",\"to\":\"c_1a2b3c4d5e6f\"},{\"field\":\"to\",\"to\":\"planned\"}]}",
      "at": "2026-09-10T15:22:46.123Z",
      "id": 1042
    }
  ],
  "total": 5012,
  "limit": 100,
  "offset": 0
}
  • items — новые сверху (at DESC). total — полное число по фильтру (без limit/offset).
  • tenantName — имя пользователя события (join с реестром); null, если пользователя нет/не разрешено.
  • changes — человекочитаемые изменения параметров события (см. «Детали события»); [], если деталей нет.
  • detailJsonстрока сырого JSON деталей события (без секретов) для спойлера; может быть null.

Детали события (changes)

Детали события хранятся как JSON вида { "changes": [ { "field": "<код>", "from": "<было>", "to": "<стало>" } ] }, где field — стабильный код параметра, from — предыдущее значение (null — параметр задан впервые), to — новое. Сериализация деталей — AuditService.ToDetailJson, форма изменения — AuditDetails.Set/ AuditDetails.Change. Ядро отдаёт changes как есть; человекочитаемые названия событий, акторов, параметров и значений — в ресурсах интерфейса (ru.js: operator.event/operator.actor/operator.field/operator.value). Прежние записи плоского формата ({ "login": "..." }, пары old*/new*) читаются обратно совместимо.

Коды: 200, 401.


Расширение GET /api/operator/audit

К прежним фильтрам (eventType, actorType, tenantId, from, to, limit) добавлены:

Параметр Тип Обяз. Описание
actorId uuid нет фильтр по идентификатору актора
offset int нет смещение страницы (≥0, дефолт 0)

Ответ — прежний { "items": [...], "total": n } (поля items/total без изменений; форма записи — как в ленте действий выше). Коды: 200, 401.


Операторские настройки: глобальные ключи Telegram

Ключи приложения Telegram (api_id/api_hash) задаются оператором глобально (ТЗ §4.1/§8.1), едины для всех тенантов. Тенант их не видит и не задаёт (ключ tgKeys удалён из GET/PATCH /api/settings). Хранилище — системная таблица public.global_settings (ключ telegramKeys), apiHash хранится зашифрованным и наружу не отдаётся.

GET /api/operator/settings/telegram-keys

Маскированный снимок глобальных ключей.

200

{
  "apiId": "1234567",
  "apiHash": "abcd…mnop",
  "keysSet": true
}
  • apiId — открыт (не секрет; пусто — ключи не заданы оператором).
  • apiHashмаска (пусто / x… / 1234…5678); открытый секрет не возвращается никогда.
  • keysSettrue, если заданы оба ключа; в GET /api/tg/status это же значение в поле keysSet.

Коды: 200, 401.

PUT /api/operator/settings/telegram-keys

Сохранение/смена глобальных ключей. Поля можно передавать по отдельности (частичное обновление): непереданное поле (null или отсутствие в JSON) сохраняет текущее значение. Если ключей ещё нет, оба поля обязательны.

Тело

{ "apiId": "1234567", "apiHash": "abcdefghijklmnop" }   // полное обновление
{ "apiId": "7654321" }                      // только apiId — apiHash сохраняется
{ "apiHash": "newsecrethash12" }            // только apiHash — apiId сохраняется
  • apiId — если передан, строго 5–9 цифр; если не передан, берётся текущий (null = «не менялось»).
  • apiHash — если передан, непустой секрет (не маска и без префикса enc:), шифруется перед сохранением; если не передан, берётся текущий зашифрованный секрет.
  • Явное пустое значение ("") считается невалидным, а не «не менялось».

200 — маскированный снимок (форма как у GET).

Ошибки

  • 400 { "detail": "Укажите api_id и api_hash" } — не передано ни одного поля.
  • 400 { "detail": "Ключи ещё не заданы — укажите и api_id, и api_hash" } — частичное обновление, но ключей ещё нет (нельзя дополнить отсутствующее значение).
  • 400 { "detail": "api_id должен состоять из 5–9 цифр" }
  • 400 { "detail": "Укажите непустой api_hash" }
  • 401 { "detail": "Требуется вход оператора" }

Аудит: событие telegram_keys_changed (актор operator, tenantId: null, детали {apiId, apiHashSet} — без секрета).

Примечание для вкладки Telegram у тенанта: GET /api/tg/status остаётся (подключение аккаунта), поле keysSet отражает глобальные ключи; команды start-phone/start-qr без ключей отвечают 400 { "detail": "Ключи Telegram не заданы оператором" }.