ci / build-test (push) Canceled after 0s
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 зелёные.
253 lines
12 KiB
Markdown
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 не заданы оператором" }`.
|