docs(task): A21 — четыре недостающих экрана веб-интерфейса

Веб покрывает пять экранов из девяти. Не хватает Аналитики, Состояния,
Журнала событий и Настроек.

Данные для первых двух уже в снапшоте: metrics.telemetry (19 вызовов,
15 отказов, латентность p50/p95/max) и metrics.host плюс readiness.
Для двух других источника в API нет вовсе — нужны GET /api/events
(EventLogService в backend есть) и GET /api/settings без секретов.

Зона Flash на это задание расширена на router/web/** целиком, включая
server.py: A20 в web/ не заходит, конфликта не будет.

Обнаружение моделей в задание НЕ включено, хотя A18 его пропустил:
причина оказалась глубже и лежит в авторизации agy, это чинит A20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-23 22:49:45 +07:00
parent 187f181aec
commit 972e34911c

View file

@ -0,0 +1,162 @@
# Задание 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`.