platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View 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`.