# Задание A21 (Antigravity Flash): довести веб-интерфейс до паритета с десктопом

## Дата поступления
2026-08-24

## База
Проверочный HEAD на момент выдачи: **`187f181`**.

## Ветка
`antigravity/web-parity`

---

## Порядок работы с git

```
cd <каталог репозитория>; git fetch origin --prune; git status
git checkout main; git pull --ff-only origin main
git checkout -b antigravity/web-parity
git commit -m "..."          <- сначала коммит
git push -u origin antigravity/web-parity
```

В конце:

```
git status                                        <- дерево чистое
git log --oneline -1 origin/antigravity/web-parity  <- ваш финальный коммит
```

---

## Что принято по A18

Действие `set_model` сделано и устроено правильно: живёт в `action_handler`, валидирует модель по списку провайдера, десктоп и веб зовут одно и то же.

**Но обнаружение моделей (P0-2) не сделано вовсе** — `model_discovery_service` и зонд не менялись, ручного обновления нет, кэша на диске нет. Из-за этого `set_model` отклоняет **любую** модель, включая настоящую: сравнивать не с чем.

```
do_set_model('ag-w2','gemini-3.1-pro-high')
-> (False, "список моделей провайдера ещё не получен")
```

Это второе задание подряд, где пункт про обнаружение остался нетронутым. **В это задание он не включён** — выяснилось, что причина глубже и лежит в авторизации: `agy models` отвечает «Please sign in» при шести валидных OAuth-профилях, потому что Hub не пишет `oauth_creds.json` в формате, который CLI ожидает. Это чинит **A20**, и обнаружение доделывается там же, после авторизации.

Отдельно скажу прямо, потому что это повторяется: **пропущенный пункт нужно называть в отчёте пропущенным.** Не «сделано», не молчание — просто «не успел» или «не смог, потому что». Один такой абзац экономит раунд.

## Что принято по A16 — и это важно для нового задания

Веб-клиент был лучшей работой за все раунды. Экран «Аккаунты» с квотами, пулами и периодами, честный индикатор источника данных, восемь содержательных скриншотов. Это задание — продолжение той же работы.

---

## Задача: четыре недостающих экрана

Веб покрывает пять экранов из девяти:

```
есть:  Аккаунты, Обзор, Маршрутизация, Модели и провайдеры, Команда
нет:   Аналитика, Состояние, Журнал событий, Настройки
```

Цель — паритет, чтобы десктоп можно было наконец перестать тащить второй веткой. Пока паритета нет, **десктоп не трогаем**: `router/ui/**` остаётся рабочим.

## P0-1. Аналитика — данные уже есть

`snapshot.metrics.telemetry` заполнена настоящими измерениями. С машины владельца:

```json
"global": {
  "window_seconds": 86400, "total_calls": 19,
  "successful_calls": 4, "failed_calls": 15,
  "call_share": 1.0, "error_rate": 0.7895,
  "latency_p50_ms": 1.4, "latency_p95_ms": 81631.1, "latency_max_ms": 81777.7,
  "total_prompt_tokens": null, "total_completion_tokens": null, "total_tokens": null
}
```

Показать: вызовы, доля ошибок, латентность p50/p95/max, разрезы по провайдерам и ролям (они рядом в том же объекте).

**Токены равны `null` — это не ноль.** Провайдеры их не отдают. Показывать «Н/Д» с причиной, ни в коем случае не «0».

Обратите внимание на сами числа: 15 отказов из 19 и p95 в 81 секунду — это следствие сломанной авторизации `agy`, которую чинит A20. Экран должен честно показывать такую картину, а не сглаживать её.

## P0-2. Состояние — данные тоже есть

`snapshot.metrics.host`:

```json
"cpu_percent": 11.8, "memory_percent": 75.5, "memory_used_mb": 9077.9,
"disk_percent": 53.7, "disk_used_gb": 255.7, "net_speed_mbps": null
```

Плюс `snapshot.readiness`: `state`, `title_ru`, `summary_ru`, `roles_ready_count` / `total_roles`, `accounts_connected_count` / `total_accounts`, `providers_ready_count` / `total_providers`, `warnings`.

`net_speed_mbps` равен `null` — показывать «Н/Д», не ноль. Список `warnings` вывести целиком: это готовности ради него и считаются.

## P0-3. Журнал событий — источника в API нет, его нужно добавить

Проверено: **событий в снапшоте нет ни в каком виде.** В backend они есть — `EventLogService` в `unified_health.py` с методом `get_events(limit, category)`.

Требуется новый эндпоинт:

```
GET /api/events?limit=<n>&category=<необязательно>
-> {"events": [{"timestamp","category","message","details","level"}]}
```

Отдавать в обратном хронологическом порядке, с разумным пределом по умолчанию. **Секреты в события не попадают** — существующий тест на отсутствие секретов в ответах должен продолжать проходить, и на новый эндпоинт его нужно распространить.

На экране: лента с фильтром по уровню и категории и поиском по тексту.

## P0-4. Настройки — текущих значений в API тоже нет

Действие `save_settings` существует, а прочитать текущие значения через API нельзя: в снапшоте их нет.

Требуется:

```
GET /api/settings  -> текущее содержимое hub_settings.json
```

**Без секретов.** Поле `web_api_token` наружу не отдавать никогда — ни целиком, ни частично. Отдавать признак «токен задан / не задан».

На экране: параметры, выбор темы, интервал обновления квот, пути. Сохранение — через существующее действие `save_settings`, второй реализации не заводить.

## P0-5. Контракт

Оба новых эндпоинта — в `docs/web-api/CONTRACT.md`, с примерами ответов. Версию контракта поднять.

Это не бюрократия: контракт версии 1.0 описал каталог `static/`, но не назвал, **кто его отдаёт**, — обе стороны выполнили написанное, и в браузере был 404. Пропуск был на авторе контракта, но цена — потерянный раунд.

---

## Ограничения

- Параллельно идёт **A20** (авторизация Antigravity). Его файлы: `oauth.py`, `profile_oauth.py`, `profile_manager.py`, `agy_subprocess.py`, `adapters/**`, `model_discovery*`, `credentials.py`. **Ничего из этого не трогать.**
- **Ваша зона на это задание расширена**: `router/web/**` целиком, включая `server.py` — там нужны два новых эндпоинта. A20 в `web/` не заходит, конфликта не будет.
- `router/ui/**` не трогать: десктоп остаётся рабочим до паритета.
- Правило честности без исключений: `null` — это «Н/Д» с причиной, а не ноль. Отличать «данных нет» от «данные грузятся» — механизм `is_loading` уже работает на «Аккаунтах», используйте его же.
- Секреты не отдавать ни в одном новом эндпоинте.
- Тег `v0.1.1` не создавать.

## Критерии приёмки

1. Ветка в `origin`, финальный коммит виден, `git status` чист.
2. Четыре экрана работают на данных: Аналитика, Состояние, Журнал событий, Настройки.
3. `GET /api/events` и `GET /api/settings` реализованы и описаны в контракте; версия контракта поднята.
4. Тест на отсутствие секретов распространён на новые эндпоинты и проходит; `web_api_token` наружу не отдаётся.
5. Ни одно `null` не показано как ноль; у каждого «Н/Д» есть причина.
6. Ни один файл зоны A20 не изменён; `router/ui/**` не тронут.
7. **Скриншоты всех четырёх экранов с настоящими данными.** Открыть и посмотреть перед тем, как прикладывать: в прошлый раз три из восьми оказались пустым чёрным кадром.
8. `ruff check .` чисто; релизный гейт не ухудшен.
9. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`. На `main` сейчас 342 passed, 2 skipped.
10. **Если какой-то пункт не сделан — сказать об этом прямо**, с причиной.

## Главное

После этого задания веб покрывает всё, что умеет десктоп, и владелец сможет пользоваться Hub на своём сервере с Ubuntu, не запуская окно по SSH. Это же условие для того, чтобы перестать поддерживать два интерфейса — а интерфейс был узким местом каждого раунда этого проекта.

## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`. Сдано только после появления коммита в `origin`.
