docs(tasks): A9 и B8 по итогам живого прогона владельца

A9 (Antigravity): миграция профилей в существующий router_profiles.yaml —
корень жалобы «при подключении грока ошибка»; квоты для codex и opencode;
служба обнаружения моделей с кэшем и фоновым обновлением; отказ от
выдуманных списков моделей. Плюс четыре утверждения отчёта A8, не
подтвердившиеся проверкой: профили claude/grok до пользователя не дошли,
проверка is_expired была мертва, dist в .gitignore, флаг /reinstall
не используется.

B8 (Codex): выбор модели агента, компактные карточки аккаунтов без
раскрытия, кликабельная карточка целиком, окно настроек роли, снятие
правой панели, устранение жёстких срезов диаграммы, решение по
дублирующим разделам, причина у каждого Н/Д.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-23 01:20:23 +07:00
parent b625b0f3e5
commit 6494c5198a
3 changed files with 1543 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,223 @@
# Задание A9 (Antigravity): миграция конфигурации, квоты остальных провайдеров, реальные модели
## Дата поступления
2026-08-23
## База
Проверочный HEAD на момент выдачи: **`b625b0f`**. Обязательно обновить локальную копию — см. следующий раздел.
## Ветка
`antigravity/quotas-models-migration`
---
## Перед началом: обновить локальную копию
Вы работаете на другой машине и пушите прямо в git. `main` ушёл далеко вперёд вашей базы: в него влиты и A8, и работа Codex (граф маршрутизации, живые квоты, дефекты живого прогона), и правки ревьюера.
```
cd <каталог репозитория>; git fetch origin --prune; git status
```
Если рабочее дерево чистое:
```
git checkout main; git reset --hard origin/main
```
Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD` и указать его в отчёте. Не считать `b625b0f` актуальным автоматически.
---
## Что принято по A8
Проверено исполнением:
- **Самолечение запуска работает.** `launcher_bootstrap` импортируется, `check_missing_dependencies()` на чистой машине возвращает `[]`, все пять функций на месте.
- **`find_free_slot` больше не выдумывает идентификаторы.** Проверено по всем пяти провайдерам мастера: возвращается либо существующий профиль, либо `None`. Это было главным дефектом A8 и он закрыт.
- **Экран переустановки в мастере есть** и подключён к `SetupEngine.IsInstalled` (`HermesHubSetup.cs:714`), с кнопкой «Переустановить» и показом версий.
- **Зеркальное развёртывание реализовано**: `MirrorDirectoryRecursive` заменил копирование. Проверено на живой машине владельца — при зеркалировании удалились девять устаревших файлов, включая четыре мёртвых модуля, которые мы удаляли из репозитория ещё в прошлых раундах.
- Тесты `test_deployment_doctor.py` проходят, ruff чисто.
Работа хорошая. Но в отчёте три утверждения, которые проверку не прошли, — читайте следующий раздел, это важнее похвалы.
---
## P0-0. Три утверждения отчёта A8, не подтвердившиеся проверкой
Это не придирки к формулировкам. Каждое из трёх означает, что заявленная функция у владельца не работает.
### 1. Профили Claude и Grok до пользователя не дошли
Отчёт: «добавлены по 3 профиля… всего 22 профиля».
Факт на живой машине владельца:
```
профилей в конфиге: 16
antigravity 10
openai-codex 3
opencode-go 3
claude 0
grok 0
find_free_slot(grok) -> None
find_free_slot(claude) -> None
```
Профили добавлены во **встроенные умолчания** (`router_config.py`) и в **пример** (`router_profiles.example.yaml`). Но `load_router_config()` возвращает умолчания **только если файла нет** (`router_config.py:324`). У владельца файл есть — `%LOCALAPPDATA%\hermes\config\router_profiles.yaml`, и он побеждает. Новые встроенные профили в существующую установку не попадают никогда.
Прямое следствие — жалоба владельца **«при подключении грока ошибка»**: мастер получает `None`, показывает «свободный слот не найден», и Grok с Claude подключить невозможно в принципе.
**Требуется миграция конфигурации.** При загрузке существующего `router_profiles.yaml` профили и роли, появившиеся во встроенных умолчаниях позже, должны в него добавляться, а не игнорироваться. Условия:
- пользовательские правки не затираются: если профиль с таким `profile_id` уже есть, он остаётся как есть;
- добавление фиксируется в журнале и видно в самопроверке;
- у файла есть версия схемы, чтобы миграция была идемпотентной и не повторялась;
- перед первой записью делается резервная копия рядом с файлом.
**Тест:** взять конфиг из 16 профилей без claude/grok, выполнить загрузку, убедиться, что после неё `find_free_slot("grok")` возвращает существующий профиль, а десять профилей `antigravity` не изменились ни в одном поле.
### 2. Проверка просроченной авторизации была мертва
Отчёт: «добавлена предварительная проверка `status.get("expired")`».
Ключ в словаре называется **`is_expired`** (`profile_manager.py:414`), поэтому `status.get("expired")` всегда `None`. Проверка не срабатывала ни разу. Ваш собственный тест этого не поймал, потому что дефект прикрывала вторая проверка — в адаптере; когда при слиянии вызов адаптера из «Теста» ушёл, протухший аккаунт стал получать зелёную галочку.
Исправлено ревьюером при слиянии (`b625b0f`), трогать не нужно. Приводится как урок: тест проверял результат, достижимый двумя путями, и молчал о том, что один из них сломан.
### 3. Пересобранный установщик до владельца не доходит
Отчёт: «Перекомпилирован `dist/HermesHubSetup.exe` и обновлен `dist/checksums.txt`».
`dist/` числится в `.gitignore:9`. Через этот репозиторий бинарник не передаётся физически — он остался на вашей машине. Владелец ставит из своей локальной сборки.
**Требуется** описать в отчёте, как собранный установщик должен попадать к владельцу: публикация в `hermes-hub-releases`, снятие `dist/` с игнорирования, или сборка на стороне владельца одной командой. Выберите один способ и обоснуйте. Пока способа нет, утверждать «установщик обновлён» нельзя.
### 4. Флаги `/reinstall` и `/repair` разбираются, но ни на что не влияют
Отчёт: «Поддержан флаг командной строки `/reinstall` (и алиас `/repair`)».
`HermesHubSetup.cs:968``bool isRepair = false;`, присваивается на строке 975 и **больше не используется нигде**. Это видно даже компилятору:
```
HermesHubSetup.cs(968,18): warning CS0219: Переменной "isRepair" присвоено значение,
но оно ни разу не использовалось
```
Запуск с `/reinstall` без `/silent` просто открывает обычный мастер. Либо реализовать тихую переустановку с кодами возврата, либо убрать флаг и не заявлять его.
---
## P0-1. Квоты для OpenAI Codex и OpenCode Go
Жалобы владельца: **«лимиты не подтягиваются, всё стоит Н/Д»** и **«у опенкода тоже нет лимитов»**.
Для Antigravity это уже решено — Codex реализовал живой опрос `retrieveUserQuotaSummary` у Google. Проверено на шести авторизованных аккаунтах владельца, данные настоящие и разные:
```
ag-w2 Claude/GPT — неделя remaining=37.4 used=62.6 source=provider_api
ag-w3 Claude/GPT — неделя remaining=90.1 used= 9.9 source=provider_api
ag-w1 Gemini — неделя remaining=99.7 used= 0.3 source=provider_api
```
Для двух других провайдеров осталась заглушка `_generate_baseline_snapshot` — все поля `None`:
```
opencode-go:opengo-1 source=provider_api
reason = "нет живого ответа от лимитов OpenCode Go"
Общий 5 часов / Недельный / Месячный: remaining=None
```
**Требуется** довести до реальных данных `openai-codex` и `opencode-go` по тому же образцу: опрос настоящего эндпоинта провайдера с использованием сохранённых учётных данных, обновление токена при 401, `source="provider_api"` только когда числа действительно измерены.
Правило честности прежнее и оно важнее полноты: **если провайдер данных не отдаёт — `None` и внятная причина, а не правдоподобное число.** Текущее поведение OpenCode Go в этом смысле правильное, оно просто неполное. Если у провайдера эндпоинта лимитов нет вовсе — это законный результат: зафиксировать в `docs/UI_STATE_CONTRACT.md` как недоступное, с причиной, чтобы интерфейс подписал честно.
**Тест:** на подготовленных учётных данных снапшот содержит измеренные значения; при ответе провайдера 401 — понятная причина и `None`; ни при каком сбое не появляется выдуманное число.
## P0-2. Служба обнаружения моделей: кэш, фон, таймаут
Владелец просит выбор моделей для агентов (жалоба 2). Интерфейс делает Codex, но опора нужна ваша.
`discover_models` есть у всех адаптеров, и для Antigravity он работает: `agy models` вернул 14 настоящих моделей.
Но вызывать его из интерфейса напрямую нельзя. Замерено на живой машине: **тот же `agy models` в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут.** Синхронный вызов заморозит окно намертво.
**Требуется** служба обнаружения моделей:
- результат кэшируется на диске рядом с конфигурацией, с временем получения;
- обновление — в фоне, с жёстким таймаутом и понятным поведением при его срабатывании;
- интерфейс получает список мгновенно из кэша плюс признак свежести;
- при пустом кэше отдаётся `None`, а не выдуманный список — интерфейс покажет «список моделей ещё не получен»;
- ошибка обнаружения не должна ронять карточку и не должна молча подставлять умолчания.
**Тест:** обнаружение с искусственной задержкой дольше таймаута не блокирует вызывающий поток и оставляет прежний кэш.
## P0-3. Выдуманные списки моделей
`auto_assigner.ensure_profile_definition` (добавлен Codex, но это ваша зона) подставляет новым профилям жёстко зашитые списки:
```python
"grok": (["grok-3", "grok-3-mini", "grok-2"], ...),
"antigravity": (["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"], ...),
```
Это тот же класс дефекта, с которым мы боролись в квотах, только про модели. И он уже даёт ложь: у живого провайдера **`gemini-3.7-flash` не существует**. Реальный список:
```
gemini-3.7-flash-high / -medium / -low
gemini-3.6-flash-high / -medium / -low
gemini-3.5-flash-high / -medium / -low
gemini-3.1-pro-high / -low
claude-sonnet-4-6, claude-opus-4-6-thinking, gpt-oss-120b-medium
```
При этом `gemini-3.7-flash` стоит в живом конфиге владельца как `default_model` роли `orchestrator`, а `gemini-3.6-flash-high` у роли `fast` — существует. То есть часть ролей настроена на несуществующую модель.
**Требуется:**
1. Списки моделей для новых профилей брать из обнаружения (P0-2), а не из литерала. Пока обнаружение не выполнено — оставлять список пустым; профиль без списка моделей честнее профиля с выдуманным.
2. Проверка конфигурации: модели, которых нет у провайдера, отмечаются в самопроверке и в контракте как недействительные, с указанием роли и профиля. Молча подставлять «похожую» модель нельзя — это решение владельца.
3. Разобраться, почему вызов с `gemini-3.7-flash` до сих пор не приводил к явной ошибке. Если провайдер молча подставляет ближайшую — это надо знать и написать в отчёте, потому что тогда владелец получает не ту модель, которую выбрал.
**Тест:** профиль, созданный при отсутствии кэша моделей, не содержит ни одного идентификатора модели; проверка конфигурации сообщает о модели, отсутствующей у провайдера.
## P1-4. Жёсткие срезы в данных для диаграммы
`dashboard_view.py:601``providers = list(snapshot.providers)[:3]`. Провайдеров пять, два молча отбрасываются. Отрисовка — зона Codex, и срез уберут там, но решение о том, сколько провайдеров вообще имеет смысл показывать и в каком порядке, принимается на стороне данных: сейчас порядок ничем не задан, поэтому какой именно провайдер исчезнет — дело случая.
Задать явный, устойчивый порядок провайдеров в снапшоте (например, по числу авторизованных профилей, затем по алфавиту) и описать его в контракте.
## P1-5. Остаток по YAML
Внутренние комментарии `router_profiles.yaml` по-прежнему теряются (7 → 2). Пункт висит с A7 и в A8 не закрыт. Либо полный round-trip, либо статус «частично» с перечнем теряемого — в контракте и в отчёте. С учётом P0-0.1 это стало важнее: миграция будет писать в этот файл, и терять при каждой записи комментарии владельца нельзя.
---
## Ограничения
- Граница: зона Codex — `src/antigravity_provider/router/ui/**`, `tests/test_ui_*.py`. По `hermes_hub_app.py` действует прежнее исключение для backend-функций вроде `do_test_profile`, но не для представления.
- Никаких чисел и идентификаторов без измерения. Нет данных — `None` и причина.
- Тег `v0.1.1` не создавать.
- Резервная копия `router_profiles.yaml` перед первой записью миграции — обязательна.
## Критерии приёмки
1. Ни один файл зоны Codex не изменён.
2. На существующем конфиге без claude/grok после загрузки `find_free_slot` для обоих возвращает существующий профиль; десять профилей `antigravity` не изменены; проверено тестом.
3. Миграция идемпотентна и не теряет пользовательские правки и комментарии.
4. Квоты `openai-codex` и `opencode-go` приходят измеренными либо `None` с причиной; ни одного выдуманного числа; проверено тестом на обоих исходах.
5. Обнаружение моделей кэшируется, обновляется в фоне и не блокирует вызывающий поток при таймауте; проверено тестом с искусственной задержкой.
6. Новые профили не содержат выдуманных моделей; проверка конфигурации сообщает о несуществующих моделях в ролях владельца.
7. `/reinstall` либо работает с кодами возврата, либо удалён; предупреждение CS0219 при сборке отсутствует.
8. В отчёте назван конкретный способ доставки установщика владельцу.
9. Прогон **в обоих окружениях**; обе команды и оба результата в отчёте.
10. `ruff check .` чисто. Про релизный гейт: он **красный на `main` уже сейчас** (проверка 4 падает не по вашей вине). Указать в отчёте его состояние до и после ваших правок; ухудшать нельзя.
11. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`.
## Главное
Владелец сказал: «надо чтобы хаб уже заработал». Ядро работает — живой каскад отказоустойчивости в журнале это доказал, и квоты Antigravity теперь настоящие. Осталось, чтобы не работающее выглядело как не работающее, а не как Н/Д без объяснений, и чтобы провайдер, который он хочет подключить, подключался.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`.

View file

@ -0,0 +1,183 @@
# Задание B8 (Codex): выбор моделей, компактные аккаунты, чистка дублирующих разделов
## Дата поступления
2026-08-23
## База
Проверочный HEAD на момент выдачи: **`b625b0f`**. Обязательно обновить локальную копию — см. следующий раздел.
## Ветка
`codex/cockpit-usability`
---
## Перед началом: обновить локальную копию
Ваша ветка `codex/usability-fixes` **отправлена в git ревьюером и влита в `main`** (`b625b0f`). Она лежала только в локальной папке и нигде не была сохранена — если бы диск отказал, работа четырёх часов пропала бы. Впредь пушьте ветку сразу после первого коммита, до всякой готовности: ветка в `origin` ничего не ломает, а несохранённая работа теряется.
```
cd <каталог репозитория>; git fetch origin --prune; git status
git checkout main; git reset --hard origin/main
```
Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD`. Не считать `b625b0f` актуальным автоматически: параллельно идёт задание A9 у Antigravity.
---
## Что принято
Проверено исполнением, и это лучшая работа за все раунды.
**Живые квоты Antigravity — сделано по-настоящему.** Вы не стали рисовать заглушку, а нашли и подключили настоящий эндпоинт `retrieveUserQuotaSummary` с обновлением OAuth-токена при 401. Запуск на шести аккаунтах владельца:
```
ag-w2 Claude/GPT — неделя remaining=37.4 used=62.6 source=provider_api
ag-w3 Claude/GPT — неделя remaining=90.1 used= 9.9 source=provider_api
ag-w4 Gemini — неделя remaining=100.0 used= 0.03 source=provider_api
```
Числа настоящие, у каждого аккаунта свои. Это закрывает главную жалобу владельца и делает продукт тем, ради чего он задуман.
Отдельно отмечу: **OpenCode Go отдаёт `None` с причиной**, а не правдоподобный процент. Соблазн заполнить пустоту был, и вы ему не поддались — правило честности выдержано там, где это стоило усилий.
Также принято: `edit_route` открывает настоящий редактор цепочки (`_open_route_editor_modal`), мастер проверяет слот и вносит профиль в маршрутизацию, ввод ключа API сообщает о результате вставки, граф маршрутизации на «Команде» с тестами.
### Что поправлено при слиянии
1. **Граница нарушена.** Изменены пять файлов зоны Antigravity: `quota_collector.py`, `unified_health.py`, `auto_assigner.py`, `health_tracker.py`, `account_identity.py`. Это дало конфликт с A8 в двух файлах. Правки приняты, потому что они ценные, но так делать нельзя: если бы Antigravity в тот же час переписал `quota_collector.py`, одна из работ была бы потеряна при слиянии. Нужен файл в чужой зоне — скажите, и он будет заказан отдельным заданием.
2. **`_finish` снова стал хрупким.** Ваша версия содержательнее прежней, её и взяли. Но хвост `log(...) → on_complete(...) → destroy()` вернул ровно тот дефект, который чинили сутки назад: исключение в любом из двух вызовов оставляет мастер открытым, и под `pythonw` пользователь не видит причины. Хвост обёрнут так, чтобы `destroy()` выполнялся всегда. Ранние `return` с сообщением в окне оставлены как есть — это правильное поведение.
3. **Протухший аккаунт получал зелёную галочку.** Вы переписали «Тест» на локальную проверку runtime без вызова модели — само по себе разумно. Но проверка просроченной авторизации от Antigravity была написана с опечаткой в имени ключа и не срабатывала; пока «Тест» ходил в адаптер, дефект прикрывался, а после вашей правки вскрылся. Исправлено ревьюером.
---
## Что говорит владелец сейчас
Дословно, одиннадцать пунктов. Разобрано по коду; ниже только ваша зона.
Сначала важное: **владелец тестировал устаревшую развёрнутую копию.** Ревьюер зеркально развернул свежий код на его машину (удалено девять устаревших файлов, добавлено два новых, обновлено пятнадцать). Поэтому часть жалоб уже закрыта вашей работой и требует только перепроверки:
| Жалоба | Состояние |
|---|---|
| 1. Лимиты не подтягиваются | Закрыто для Antigravity. Для codex и opencode — задание A9 |
| 4. «Настроить» не работает | Закрыто, открывается редактор цепочки |
| 8. OpenCode не даёт вставить API | Закрыто, поле и «Вставить» с обратной связью |
| 9. Квота не отображается в аккаунтах | Закрыто в части данных; вид — ниже, пункт P0-2 |
| 11. У OpenCode нет лимитов | Провайдер их не отдаёт; причина показывается. Дожимает A9 |
| 3а. Нет «Быстрого» и «Исследователя» на «Обзоре» | Проверено на свежем коде: все пять ролей помещаются. Это был эффект устаревшей копии |
Остальное — работа.
## P0-1. Выбор модели для агента
Жалоба: **«нет возможности выбрать модели для агентов. и надо сделать»**.
Сейчас модель только показывается (`team_view.py:690`, `dashboard_view.py:614`) и нигде не выбирается. Ни одного элемента управления моделью в интерфейсе нет.
Требуется выбор модели для роли и для профиля, сохраняемый в конфигурацию.
Источник списка — служба обнаружения моделей, которую делает Antigravity в A9 (кэш на диске, обновление в фоне, отметка свежести). **Синхронно вызывать `discover_models` из интерфейса нельзя:** замерено, что `agy models` в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут. Окно замёрзнет.
До появления службы: читать список из кэша, если он есть; при пустом кэше показывать «список моделей ещё не получен» и кнопку обновления, работающую в фоне. **Не подставлять список литералом**у владельца в конфигурации уже стоит `gemini-3.7-flash`, которой у провайдера не существует, и появилась она именно так.
Если выбранная модель отсутствует в обнаруженном списке — пометить её и объяснить, но не менять молча за пользователя.
## P0-2. Аккаунты: компактно, как в Cockpit Tools, без раскрывающихся карточек
Жалобы: **«аккаунты должны выглядеть как у кокпит тулс, компактно»** и **«вид не тот, не надо делать раскрывающееся окно»**.
Требуется:
- плотный список фиксированной высоты: провайдер, идентичность, роль, состояние авторизации, квота — в одной строке, без раскрытия;
- квота видна сразу, числом и полосой, с пометкой периода (`Claude/GPT — неделя`), потому что у Antigravity пулов четыре и «просто процент» вводит в заблуждение;
- шестнадцать аккаунтов должны читаться без прокрутки внутрь карточек;
- дельта-отрисовку по стабильным ключам не терять — она уже есть и это главное преимущество перед прошлой версией.
Документ `COCKPIT_TOOLS_ARCHITECTURE_COMPARISON.md` в корне репозитория описывает целевую модель обновления (`splice-by-index` вместо пересборки всех карточек). Это архитектурное сравнение, а не макет: берите из него принцип обновления и плотность, а не буквальную вёрстку.
## P0-3. Вся карточка кликабельна
Жалоба: **«чтобы полностью окно было активным, чтобы не выцеливать нажимать 3 точки справа в углу»**.
Клик по любому месту карточки аккаунта или агента открывает её детали. Меню «три точки» остаётся для второстепенных действий, но перестаёт быть единственным входом. Курсор меняется на указатель, есть состояние наведения, работает клавиатура (Tab и Enter).
## P0-4. Карточка роли открывает настройки роли
Жалоба: **«при нажатии на окно (например кодер) нужно чтобы выходило окно с настройками кодера, где выходят сразу лимиты, и можно поменять модель агента и сменить аккаунт»**.
Клик по роли на «Обзоре» и на «Команде» открывает окно роли, в котором сразу видно:
- цепочка отказоустойчивости: основной профиль и резервы по порядку, какой активен сейчас;
- квоты активного профиля — числом, по пулам, с временем сброса;
- смена модели (P0-1);
- смена аккаунта, то есть перестановка профиля в цепочке — через существующий `AutoAssigner`, второй редактор маршрутизации не создавать;
- причина последнего переключения, если она была.
## P0-5. Убрать «Статус в реальном времени», центр растянуть
Жалоба: **«блок справа не понятный. Статус в реальном времени. там ничего не отображается, просто левые цифры. можно пока его убрать, а центральный блок растянуть на все окно»**.
Убрать правую панель (`dashboard_view.py:447`) и отдать её ширину схеме маршрутизации. Владелец прав по существу: панель занимает треть экрана и показывает то, что либо дублируется, либо не измеряется.
Если внутри неё есть блок с настоящими измеренными данными — не выбрасывать, а перенести в строку KPI и сказать в отчёте, какой именно и на каком поле контракта он основан.
## P0-6. Жёсткие срезы в диаграмме
`dashboard_view.py:601``providers = list(snapshot.providers)[:3]` при пяти провайдерах: `claude` и `grok` молча отбрасываются. Строкой ниже `agents[:5]` — сейчас ролей ровно пять и срез не виден, но добавление шестой роли её потеряет.
Диаграмма должна следовать данным: сколько ролей и провайдеров в снапшоте, столько узлов. Порядок провайдеров задаёт Antigravity в A9.
## P0-7. «Провайдеры» и «Квоты и лимиты» дублируют «Аккаунты»
Жалоба: **«провайдеры и квоты и лимиты вообще не понятно для чего нужны, там все тоже, что и в аккаунты»**.
Владелец прав: после того как квоты появились прямо в карточках аккаунтов, отдельный раздел квот потерял смысл.
Требуется решение, а не сохранение обоих на всякий случай:
- **«Квоты и лимиты»** — либо убрать из навигации, либо оставить только то, чего нет в «Аккаунтах»: сводка по провайдеру целиком, история расхода, ближайшие сбросы. Если такого содержания нет — убрать.
- **«Провайдеры»** — оставить то, что относится к провайдеру, а не к аккаунту: доступность runtime, обнаруженные модели, версия CLI, состояние авторизации в целом.
В отчёте перечислить, что перенесено, что удалено и почему. Пустой раздел не оставлять — правило прежнее: либо содержание, либо нет пункта.
## P0-8. Объяснить Н/Д в маршрутизации
Жалоба: **«что означает н/д в маршрутизация запросов»**.
Это претензия не к честности, а к молчаливости: владелец видит `Н/Д` и не знает, это поломка, ненастроенное или неизмеримое.
Везде, где стоит `Н/Д`, должна быть доступна причина — подсказкой при наведении и текстом рядом, если место позволяет. Формулировки конкретные: «нет телеметрии: роль ещё не вызывалась», «провайдер не отдаёт лимиты», «аккаунт не подключён». Данные для этого есть — `unavailable_reason` в снапшоте квот уже заполняется.
---
## Ограничения
- Граница: ваша зона — `src/antigravity_provider/router/ui/**`, `hermes_hub_app.py` в части представления, `tests/test_ui_*.py`. **Файлы зоны Antigravity не трогать** — понадобился, закажите.
- Не выдумывать модели, числа и идентификаторы. Нет данных — `Н/Д` с причиной либо блок отсутствует.
- Три темы сохранить, дельта-отрисовку не терять.
- Мастер не ломать: шесть потоков подключения, трёхэлементная распаковка `start_profile_oauth`, `destroy()` в `_finish` выполняется всегда.
- Тег `v0.1.1` не создавать.
- **Ветку запушить сразу после первого коммита.**
## Критерии приёмки
1. Ни один файл зоны Antigravity не изменён.
2. Модель агента выбирается и сохраняется; список берётся из кэша, интерфейс не блокируется; при пустом кэше показано «список ещё не получен», а не литерал.
3. Карточка аккаунта компактна, фиксированной высоты, без раскрытия; квота видна сразу с указанием пула и периода; шестнадцать аккаунтов читаются без внутренней прокрутки.
4. Клик по любому месту карточки открывает детали; «три точки» перестали быть единственным входом; работает Tab и Enter.
5. Клик по роли открывает окно роли с цепочкой, квотами, сменой модели и сменой аккаунта.
6. Правая панель убрана, схема занимает освободившуюся ширину; в отчёте сказано, что перенесено в KPI и на каком поле контракта основано.
7. Диаграмма показывает все роли и всех провайдеров из снапшота; жёстких срезов не осталось.
8. Принято решение по «Провайдерам» и «Квотам»; в отчёте перечислено перенесённое и удалённое.
9. У каждого `Н/Д` доступна причина.
10. Прогон **в обоих окружениях** — без UI-зависимостей и с `customtkinter`/`pillow`/`psutil`; обе команды и оба результата в отчёте.
11. `ruff check .` чисто. Релизный гейт **уже красный на `main`** (проверка 4 падает не по вашей вине) — указать состояние до и после; ухудшать нельзя.
12. **Скриншоты живого сценария с настоящими данными**: «Аккаунты» с видимыми квотами, окно роли, выбор модели, «Обзор» без правой панели. Пустых состояний не присылать.
## Главное
Квоты вы уже сделали настоящими — самое трудное позади. Осталось, чтобы владелец мог управлять тем, что видит: выбрать модель, сменить аккаунт у роли, окинуть взглядом шестнадцать аккаунтов без прокрутки и в каждом непонятном месте получить ответ, почему там прочерк.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`.