platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
171
docs/phase-e/E6_EGRESS_IDENTITY_API_CONTRACT.md
Normal file
171
docs/phase-e/E6_EGRESS_IDENTITY_API_CONTRACT.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# E6 API-контракт: egress identity (IP + country)
|
||||
|
||||
Дата: 2026-03-10
|
||||
Статус: draft (E6.1 freeze)
|
||||
Владелец: Engineering
|
||||
|
||||
## 1) Цель
|
||||
- Ввести единый backend-контракт для определения текущей egress-идентичности для любого движка.
|
||||
- Избежать дублирования логики в desktop/web/mobile: UI только читает готовые поля из Go API.
|
||||
- Дать унифицированные поля для показа `IP + страна` и построения флага в UI из `country_code`.
|
||||
|
||||
## 2) Область и ограничения
|
||||
- Источник истины: только Go-ядро.
|
||||
- UI не делает внешние IP/Geo запросы напрямую.
|
||||
- Поддерживаемые scope:
|
||||
- `adguardvpn`
|
||||
- `transport:<client_id>`
|
||||
- `system`
|
||||
- Первичный формат ответа: `HTTP 200 + ok/message` (совместимо с текущим API-паттерном).
|
||||
|
||||
## 3) Модель данных
|
||||
|
||||
### 3.1 EgressIdentity
|
||||
```json
|
||||
{
|
||||
"scope": "transport:sg-realnetns",
|
||||
"source": "transport",
|
||||
"source_id": "sg-realnetns",
|
||||
"ip": "203.0.113.10",
|
||||
"country_code": "SG",
|
||||
"country_name": "Singapore",
|
||||
"updated_at": "2026-03-10T07:20:00Z",
|
||||
"stale": false,
|
||||
"refresh_in_progress": false,
|
||||
"last_error": "",
|
||||
"next_retry_at": ""
|
||||
}
|
||||
```
|
||||
|
||||
Пояснения:
|
||||
- `scope`: нормализованный ключ области.
|
||||
- `source`: `adguardvpn|transport|system`.
|
||||
- `source_id`: для `transport` это `client_id`, иначе пусто.
|
||||
- `ip`: egress IP для выбранного scope.
|
||||
- `country_code`: ISO-2 uppercase (например `US`, `SG`).
|
||||
- `country_name`: человекочитаемое имя страны.
|
||||
- `updated_at/stale/refresh_in_progress/last_error/next_retry_at`: SWR-метаданные.
|
||||
|
||||
### 3.2 EgressIdentityResponse
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "ok",
|
||||
"item": {
|
||||
"scope": "adguardvpn",
|
||||
"source": "adguardvpn",
|
||||
"source_id": "",
|
||||
"ip": "198.51.100.5",
|
||||
"country_code": "NL",
|
||||
"country_name": "Netherlands",
|
||||
"updated_at": "2026-03-10T07:20:00Z",
|
||||
"stale": false,
|
||||
"refresh_in_progress": false,
|
||||
"last_error": "",
|
||||
"next_retry_at": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 EgressIdentityRefreshRequest
|
||||
```json
|
||||
{
|
||||
"scopes": ["adguardvpn", "transport:sg-realnetns"],
|
||||
"force": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 EgressIdentityRefreshResponse
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "refresh queued",
|
||||
"count": 2,
|
||||
"queued": 1,
|
||||
"skipped": 1,
|
||||
"items": [
|
||||
{
|
||||
"scope": "adguardvpn",
|
||||
"status": "queued",
|
||||
"queued": true,
|
||||
"reason": ""
|
||||
},
|
||||
{
|
||||
"scope": "transport:sg-realnetns",
|
||||
"status": "skipped",
|
||||
"queued": false,
|
||||
"reason": "throttled or already fresh"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 4) Endpoint-ы
|
||||
|
||||
### 4.1 `GET /api/v1/egress/identity`
|
||||
Назначение:
|
||||
- Получить текущий snapshot egress identity для одного scope.
|
||||
|
||||
Query:
|
||||
- `scope` (required): `adguardvpn|transport:<id>|system`
|
||||
- `refresh=1` (optional): best-effort trigger фонового refresh перед возвратом snapshot.
|
||||
|
||||
Response:
|
||||
- `200 + EgressIdentityResponse`.
|
||||
|
||||
Ошибки запроса:
|
||||
- `400` при невалидном `scope`.
|
||||
|
||||
### 4.2 `POST /api/v1/egress/identity/refresh`
|
||||
Назначение:
|
||||
- Поставить refresh в очередь для одного или нескольких scope без блокировки UI.
|
||||
|
||||
Request body:
|
||||
- `scopes[]` optional (если пусто -> refresh всех известных scope);
|
||||
- `force` optional (`true` игнорирует freshness TTL, но уважает single-flight lock).
|
||||
|
||||
Response:
|
||||
- `200 + EgressIdentityRefreshResponse`.
|
||||
|
||||
Ошибки запроса:
|
||||
- `400` bad json / invalid scope format.
|
||||
|
||||
## 5) Правила freshness/SWR
|
||||
- Refresh делается в фоне, UI получает последний cache snapshot мгновенно.
|
||||
- Для каждого scope:
|
||||
- single-flight (не запускать параллельные одинаковые refresh);
|
||||
- backoff при ошибках;
|
||||
- `stale=true` если snapshot устарел или нет новых данных;
|
||||
- `next_retry_at` выставляется при backoff.
|
||||
- Рекомендуемая стратегия UI:
|
||||
- сразу рисовать cache (`GET`),
|
||||
- отдельным действием триггерить `POST .../refresh`,
|
||||
- обновляться по SSE/ручному poll.
|
||||
|
||||
## 6) SSE событие
|
||||
Событие для подписчиков:
|
||||
- `egress_identity_changed`
|
||||
|
||||
Payload (минимум):
|
||||
```json
|
||||
{
|
||||
"scope": "transport:sg-realnetns",
|
||||
"ip": "203.0.113.10",
|
||||
"country_code": "SG",
|
||||
"country_name": "Singapore",
|
||||
"updated_at": "2026-03-10T07:20:00Z",
|
||||
"stale": false,
|
||||
"last_error": ""
|
||||
}
|
||||
```
|
||||
|
||||
## 7) Отрисовка флага
|
||||
- Backend возвращает только `country_code`.
|
||||
- Флаг рендерится в UI из `country_code` (desktop/web/mobile одинаково).
|
||||
- Если `country_code` пустой/невалидный: UI показывает `N/A` без флага.
|
||||
|
||||
## 8) Минимальные критерии готовности E6.1
|
||||
- Документирован единый контракт `GET/POST` для egress identity.
|
||||
- Зафиксированы scope и поля snapshot.
|
||||
- Зафиксированы правила SWR и SSE-событие.
|
||||
- Явно указано, что флаг строится в UI из `country_code`.
|
||||
Reference in New Issue
Block a user