Files
Deal/docs/architecture/2026-09-10-operator-analytics-contract.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

253 lines
12 KiB
Markdown

# Дейл — контракт операторской аналитики и аудита действий (этап 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**
```json
{
"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**
```json
{
"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**
```json
{
"items": [
{
"eventType": "card_moved",
"actorType": "tenant",
"actorId": "1f2e3d4c-5b6a-7980-1234-56789abcdef0",
"tenantId": "aabbccdd-eeff-0011-2233-445566778899",
"ip": "203.0.113.7",
"detailJson": "{\"cardId\":\"c_1a2b3c4d5e6f\",\"to\":\"planned\"}",
"at": "2026-09-10T15:22:46.123Z",
"id": 1042
}
],
"total": 5012,
"limit": 100,
"offset": 0
}
```
- `items` — новые сверху (`at` DESC). `total` — полное число по фильтру (без `limit`/`offset`).
- `detailJson`**строка** JSON деталей события (без секретов), может быть `null`.
**Коды**: `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**
```json
{
"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) сохраняет текущее значение. Если ключей ещё нет,
оба поля обязательны.
**Тело**
```json
{ "apiId": "1234567", "apiHash": "abcdefghijklmnop" } // полное обновление
```
```json
{ "apiId": "7654321" } // только apiId — apiHash сохраняется
```
```json
{ "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 не заданы оператором" }`.