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— GuidD;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— новые сверху (atDESC).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); открытый секрет не возвращается никогда.keysSet—true, если заданы оба ключа; в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 не заданы оператором" }.