# Агрегирующие эндпоинты шлюза (`/api/agg/`) Шлюз **telemt-api** опрашивает несколько upstream [Telemt Control API](API.md) и отдаёт сводные JSON-ответы. - Большинство маршрутов агрегации используют **`GET /v1/stats/users`** на каждом сервере из конфигурации. - **`GET /api/agg/fleet-status`** дополнительно вызывает на каждом upstream **`GET /v1/health`** и **`GET /v1/system/info`** (параллельно по серверам). - **`GET /api/agg/radar-telemt-dcs`** — на каждом upstream параллельно **`GET /v1/stats/dcs`** (снимок ME / DC для панели «Радар DC»); см. [API.md](API.md) про `minimal_runtime_enabled` и поля `DcStatusData`. **Единицы трафика в агрегатах:** поля `*_megabytes` — это **двоичные мегабайты (MiB)**, 1 MiB = 1024² октетов (как у Telemt в ответе считаются октеты, шлюз делит на MiB для удобства). Доступ к **одному** инстансу по-прежнему через прокси: `GET /api/{alias}/…` (например `/api/gt1/v1/stats/users`) — там по-прежнему `total_octets` как в [API.md](API.md). ## Успешный ответ (общий контракт) ```json { "ok": true, "data": {}, "generated_at": "2026-03-30T12:00:00.000000000Z", "partial": true } ``` - **`generated_at`** — UTC, RFC3339Nano; время формирования ответа шлюза. - **`partial`** — присутствует и равно `true`, если **хотя бы один** upstream в этом запросе завершился с ошибкой (HTTP не 200, сеть, `ok: false` в теле и т.д.), либо для `fleet-status` — если не оба подзапроса (health и system/info) успешны для какой-либо ноды. Если все вызовы успешны, поле **`partial` не включается**. ## Маршруты | Метод | Путь | Описание | | --- | --- | --- | | GET | `/api/agg/summary` | Сводка по флоту, список опросов upstream, `fleet_total_megabytes` / `fleet_total_connections`. Два топа (размер задаётся `top_n`): **`top_users`** — самые «прожорливые» по суммарному трафику (MiB) по всем серверам; **`top_users_by_unique_ips`** — по максимальному `active_unique_ips` среди серверов для пользователя (как в Telemt, снимок). | | GET | `/api/agg/traffic` | Трафик по каждому пользователю в разрезе серверов: `servers..total_megabytes`. | | GET | `/api/agg/unique-ips` | Уникальные IP по пользователю: на каких серверах IP есть в active/recent списках снимка. При **`geoip.enabled`** в конфиге — из City: `country_code`, `country_name`, `city_name`, а также `latitude`, `longitude` (если доступны); при наличии ASN-БД — `asn`, `as_organization` (см. [GEOIP.md](GEOIP.md)); отключить гео для запроса: `?geo=false`. | | GET | `/api/agg/users` | Объединённый список пользователей с `by_server`, суммарным `total_megabytes` и **смерженными лимитами** (см. ниже). | | GET | `/api/agg/user/{username}` | Один пользователь в том же формате, что элементы `/api/agg/users` (без списка всех). Имя в пути: `[A-Za-z0-9_.-]+`. Ответ **`404`**, если пользователь не найден ни на одном успешном upstream. | | GET | `/api/agg/fleet-status` | По каждому алиасу: параллельно health + system/info; в `data.servers[]` — статусы подзапросов и тела `health` / `system_info` при успехе. См. [AGGREGATE_OPENAPI.yaml](AGGREGATE_OPENAPI.yaml). | | GET | `/api/agg/radar-telemt-dcs` | По каждому алиасу: параллельно `GET /v1/stats/dcs`; в `data.servers[]` — `alias`, `ok`, при успехе объект `data` (поля `middle_proxy_enabled`, `reason`, `dcs[]` с `dc`, `coverage_pct`, `rtt_ms` и т.д.). | | GET | `/api/agg/incidents` | Нормализованный snapshot инцидентов для triage-панели: `critical/warning/info`, `affected_aliases`, рекомендуемые `actions` (runbook/deep links), счётчики по severity. | Все методы — **GET**; действует тот же whitelist, что и для остального API шлюза. ### Слияние лимитов в `users` и `user/…` Поля в строке пользователя (кроме счётчиков и `by_server`): | Поле | Политика | | --- | --- | | `user_ad_tag` | Первое непустое значение при обходе серверов в **лексикографическом порядке алиаса**. | | `expiration_rfc3339` | **Самая ранняя** дата среди заданных на серверах (по разбору RFC3339 / RFC3339Nano). | | `max_tcp_conns`, `data_quota_bytes`, `max_unique_ips` | **Минимум** среди заданных на серверах (самый строгий лимит). | Ссылки `links` при `include_links=true` по-прежнему берутся из **первой успешной** записи по пользователю (как раньше). ## Query-параметры | Параметр | Где | Значение | | --- | --- | --- | | `aliases` | все | Список алиасов через запятую (например `gt1,gt2`). Если не задан — см. `aggregate.include_aliases` в YAML или все серверы из `servers`. | | `top_n` | `summary` | Размер топа пользователей (по умолчанию `10`, максимум `1000`). | | `include_links` | `users`, `user/…` | `true` — добавить сгенерированные `tg://proxy` ссылки (берётся первая успешная запись по пользователю). | | `min_total_megabytes` | `users` | Порог суммарного трафика пользователя в MiB (строго больше 0). | | `min_total_octets` | `users` | Устаревший вариант порога в октетах (если задан `min_total_megabytes`, он приоритетнее). | | `aliases` | `incidents` | Список алиасов через запятую; позволяет строить incidents snapshot по выбранной группе нод. | ## Live stream (SSE) Для оперативного режима NOC доступен поток событий: - **`GET /api/live/events`** (`text/event-stream`) - query: `aliases` (опционально, как в `/api/agg/*`) - событие: `event: snapshot` - payload: JSON со статусом флота (`healthy/degraded/critical`), `partial`, и массивом `incidents` Поток рассчитан на UI-клиент с авто-reconnect (на фронте используется экспоненциальный backoff). ## Конфигурация (опционально) ```yaml # SPA на другом origin: список разрешённых Origin или "*" (без учётных данных cookie к шлюзу). cors_allowed_origins: - "http://localhost:5173" aggregate: include_aliases: - gt1 - gt2 # Кэш только для успешных (HTTP 200) ответов /api/agg/*, ключ = путь + query. 0 = выкл. Макс. 60000 мс. cache_ttl_ms: 2000 ``` Если блок `aggregate` отсутствует или `include_aliases` пуст, по умолчанию участвуют **все** записи `servers`. Имя алиаса **`agg`** в `servers` запрещено (зарезервировано под префикс `/api/agg/`). ### CORS Если задан непустой **`cors_allowed_origins`**, шлюз для подходящего заголовка **`Origin`** добавляет заголовки CORS и отвечает на **`OPTIONS`** кодом **204** без тела (preflight). Совпадение: точное равенство строки origin или `"*"`. Whitelist IP по-прежнему применяется **до** обработки запроса. ## Ограничения - **Один и тот же `username` на разных серверах** может соответствовать разным учётным записям; агрегатор сопоставляет строки по имени — учитывайте при интерпретации сумм. - У Telemt в `UserInfo` **нет** поля «IP последний раз подключался к серверу X». В `unique-ips` поле `primary_server` заполняется **только** если ровно один сервер видит IP в `active_unique_ips_list` на момент запроса; иначе `primary_server` отсутствует или несколько серверов в списках — это снимок, не история. ## Машиночитаемый контракт Черновик схемы OpenAPI 3 для `/api/agg/*`: **[AGGREGATE_OPENAPI.yaml](AGGREGATE_OPENAPI.yaml)** (удобно для генерации типов на фронтенде). ## Примеры ```bash curl -sS "http://127.0.0.1:8080/api/agg/summary" curl -sS "http://127.0.0.1:8080/api/agg/traffic?aliases=gt1,gt2" curl -sS "http://127.0.0.1:8080/api/agg/unique-ips" curl -sS "http://127.0.0.1:8080/api/agg/users?include_links=false&min_total_megabytes=1" curl -sS "http://127.0.0.1:8080/api/agg/fleet-status" curl -sS "http://127.0.0.1:8080/api/agg/user/myuser?aliases=gt1" curl -sS "http://127.0.0.1:8080/api/agg/incidents?aliases=gt1,gt2" # SSE поток snapshot-событий (пример с curl) curl -N "http://127.0.0.1:8080/api/live/events?aliases=gt1,gt2" ```