Статус: план, код не менялся. Основа — прогон 6440 адресов (2026-10-02 13:47 – 22:29 UTC) и три отчёта в `analysis/`.
## 1. Зачем
Сейчас выводы по прогону получены ручными запросами к БД: срезы по подсетям, провалы по целям, ingress по площадкам, расхождение вердикта и проверок. Нужна страница, которая показывает то же самое постоянно, для любого прогона, без SQL.
Главное требование: переключать **отметку времени** — завершённый запуск проверки, — чтобы данные разных запусков не смешивались.
## 2. Ключевая находка: «запуска» в данных нет
`cycle_id` считается **на адрес** (`ip_registry.next_cycle`), а не на весь прогон. В БД сейчас: цикл 1 у 6431 адреса, циклы 2–4 у 9 перепроверенных. По `cycle_id` нельзя выбрать «прогон от 2 октября»: он ничего не группирует.
Группировка по времени тоже ненадёжна: прогон 6440 адресов шёл 8 ч 42 мин, а перепроверки внутри него шли в то же время.
Поэтому нужна новая сущность — **запуск** (`run`). Это единственное изменение схемы, без него переключатель невозможен.
-`run_results(run_id, registry_id, ip_address, cycle_id, verdict, aggregated_at, expected_checks, recorded_at_aggregation)`. Одна строка на адрес в запуске. Нужна, потому что вердикт сейчас хранится только в `ip_queue.overall_result` и затирается при перепроверке.
-`subnets(cidr PRIMARY KEY, label)`. Список подсетей заказчика (45 шт.) не лежит ни в БД, ни в репозитории, его задаёт администратор.
Новые колонки: `run_id` в `ip_queue` и `checks`. Индекс `idx_checks_run(run_id, registry_id)`.
### Правила запуска
1. Новый запуск открывается, когда в очередь попадает адрес, а открытого запуска нет. Автоцикл открывает запуск явно в начале фазы.
2. Пока запуск открыт, все добавленные и перепроверяемые адреса входят в него.
3. Запуск **завершается**, когда все его строки очереди в конечном состоянии (`done`, `failed`, `occupied`, отмена). Тогда `finalized_at` = последняя агрегация.
4. Перепроверка после завершения открывает **новый** запуск. Поэтому старый запуск не меняется.
5. Если адрес перепроверен внутри открытого запуска, в аналитике берётся его последний цикл этого запуска.
6. Поздние результаты (пришли после агрегации, как у 50 адресов в анализе 6.5) остаются в своём запуске: `run_id` берётся из строки очереди при записи проверки. Их число показывается отдельно.
### Заполнение уже накопленных данных
Запуски выделяются по паузе: если между окончаниями соседних циклов больше 60 минут, начинается новый запуск. Максимальная пауза в текущей БД 0,8 минуты, поэтому получится **один запуск на 6440 адресов**, 9 перепроверок войдут в него. Вердикт берётся из `ip_queue.overall_result`, для прежних циклов без него считается по проверкам и помечается как расчётный.
## 4. API (control-api, только чтение)
-`GET /api/v1/admin/analytics/runs` — список запусков: id, тип, начало, конец, адресов, доли `pass/partial/fail`, состояние. Открытый запуск виден, но помечен как идущий.
-`GET /api/v1/admin/analytics/runs/{id}` — все блоки страницы одним ответом (десятки КБ):
- **summary**: адресов; вердикты; Egress OK и Ingress OK по фактическим проверкам; доли по типам проверок; индикаторы прикладного уровня: адреса, провалившие все egress-проверки https (на запуске 02.10: 307, из них по всем 5 целям 290), и адреса, провалившие все ingress-проверки ssh (7; хотя бы с одной площадки 222); длительность и скорость; число поздних результатов; число неполных наборов.
- **partial_reasons**: причины `partial` (только egress; ingress и egress; только ingress; неполный набор; только неполный набор).
- **subnets**: по каждой подсети — адресов, `pass`, Egress OK, Ingress OK.
- **targets**: egress по цели и типу проверки (процент провалов); матрица «подсеть × цель» для выбранного типа.
- **sites**: ingress по площадке и типу проверки.
- **validators**: доля провалов egress по валидаторам.
- **errors**: классы ошибок по уровню и типу (таймаут, нет маршрута, баннер SSH и др.), классификатор по `checks.detail`.
- **data_quality**: поздние результаты, неполные наборы, адреса, где вердикт расходится с проверками.
-`GET/PUT /api/v1/admin/config/subnets` — список подсетей (замена целиком, текстом по одной в строке).
Группировка по типу проверки везде одна и та же, правило `CheckFamily` из `internal/db/models.go` (tcp-22 и tcp-443 → `tcp`). Новый тип появляется в таблицах сам.
Адрес → подсеть определяется в Go по самому длинному совпавшему префиксу (SQLite не умеет работать с CIDR). Для 6440 адресов это миллисекунды.
Расчёт идёт одним проходом по `checks` запуска (~190 тыс. строк). Результат завершённого запуска кэшируется в памяти по `run_id` и `MAX(checks.id)` запуска, чтобы поздний результат сбросил кэш.
- Два индикатора прикладного уровня (Egress https и Ingress ssh: все провалены) кликабельны: клик открывает окно со списком адресов и кнопкой «Скачать CSV». В рабочей странице список берётся из `GET /admin/analytics/runs/{id}/addresses?list=egress_https_all_failed|ingress_ssh_all_failed` (CSV собирается на сервере, `Content-Disposition: attachment`, UTF-8 с BOM), чтобы не зависеть от размера страницы.
- Каждая строка блока «Классы ошибок ingress» кликабельна: окно показывает распределение проваленных проверок класса по валидаторам, вердикт адреса, статус проверки («в вердикте» или «после вердикта») и список с кнопкой «Скачать CSV». Эндпоинт: `GET /admin/analytics/runs/{id}/errors/{class}` (JSON и `?format=csv`).
- Выбор запуска хранится в адресной строке (`?run=`), ссылку можно переслать. По умолчанию — последний завершённый запуск.
- Каждый блок рассчитан только на выбранный запуск. Данных других запусков на странице нет.
- Строка подсети, ячейка матрицы и причина `partial` — ссылки в «Реестр» с фильтром, чтобы увидеть конкретные адреса. Для этого в `GET /admin/registry` и `/registry` добавляются параметры `run`, `subnet`, `target`, `reason`; страница «Реестр» тоже учитывает выбранный запуск.
- Рисование: HTML и встроенный SVG на сервере, без библиотек графиков (в дашборде их нет). Цвета и тепловая карта — по руководству навыка `dataviz`, с тёмной темой, как в остальном дашборде.
- Пункт «Аналитика» в боковом меню (`internal/dashboard/templates/layout.html`).
- Дашборд: `internal/dashboard/handlers_analytics.go`, `templates/analytics.html`, `static/dashboard.css`, пункт меню, форма списка подсетей на `/settings`.
## 7. Поставка тремя шагами
1.**Запуски**: миграция, правила, заполнение накопленных данных. Результат: в БД один запуск на 6440 адресов. Видимых изменений в UI нет.
2.**API аналитики**: расчёт и эндпоинты.
3.**Страница**: макет выше, фильтры в реестре.
Каждый шаг отдельно собирается, тестируется и выкатывается на стенд.
## 8. Проверка
- Контрольные числа: расчёт по запуску на копии боевой БД (`rxprod-compose/capi-db`) должен дать цифры отчётов `analysis/`: 6440 адресов, 1962 `pass` / 4478 `partial`; провал egress у 4437; провал HTTPS по `packages.ubuntu.com` у 3841; 290 адресов провалили все цели; ingress провален у 224; 50 `pass` с поздними провалами; 167 неполных наборов. Если цифры не сходятся, расчёт неверен.
- Миграция на копии боевой БД: ровно один запуск, `run_id` заполнен у всех `checks` и `ip_queue`, повторный запуск миграции безопасен.
- Тесты: изоляция запусков (два запуска с разными результатами у одного адреса не пересекаются); перепроверка после завершения открывает новый запуск, внутри открытого — нет; поздний результат попадает в свой запуск; идущий запуск не выбирается; новый тип проверки (`dns`) появляется в таблицах; подсети (самый длинный префикс, адрес вне списка → «прочие»).
- Нагрузка: страница на запуске 6440 адресов, расчёт до ~1 с, повторный запрос из кэша быстрее; `EXPLAIN QUERY PLAN` по `idx_checks_run`.
- Вручную на стенде: выбрать запуск, сверить ключевые числа с отчётами, пройти по ссылке из подсети в реестр.
## 9. Допущения и ограничения
- **Валидатор у ingress-проверок.** В`checks.validator_id` для проверок пробера пусто. Валидатор адреса в цикле берётся из события `fip_associated` (поле `validator_id` в payload) по `registry_id` и `cycle_id`. На запуске 02.10 все 950 проваленных ingress-проверок нашли валидатора. Надёжнее писать `validator_id` в ingress-проверку при приёме результата; это отдельное маленькое изменение шага 1.
- **Классификатор ошибок** работает по `checks.detail` и типу проверки; на запуске 02.10 он даёт 7 классов с суммой 950 (SSH: таймаут 327, ICMP: нет ответа 276, TCP-22: таймаут 260, SSH: баннер «Not allowed» 47, SSH: нет маршрута 18, ICMP: time exceeded 15, TCP-22: нет маршрута 7). Новые тексты ошибок попадают в класс «прочее» и видны в списке.
- **Новое наблюдение для блока «Качество данных».** 844 из 950 проваленных ingress-проверок (89%) записаны после агрегации вердикта (среди всех ingress-проверок после вердикта пришло 1430 из 76644, около 1,9%). То есть таймауты приходят позже вердикта систематически, а не случайно. В отчёте анализа 6.5 это видно только для 246 провалов у адресов `pass`.
- Список подсетей даёт администратор (форма на `/settings`). Пока он пуст, подсети группируются автоматически по `/24`.
- Порог 60 минут для заполнения накопленных данных — только для старых записей, настройкой не делается.
- Глубина истории (`history_retention_cycles` > 0) удаляет старые проверки, и аналитика старых запусков теряет детали. По умолчанию 0 (хранить всё). Вердикты в `run_results` остаются.
- Перед выкладкой миграции обязательна копия `rxprod-compose/capi-db/control-api.db`.
- Вне объёма (следующий этап): сравнение двух запусков, выгрузка в CSV, оповещения.
- Вердикт системы и фактические проверки считаются отдельно и показаны рядом (по анализу 6.5 они расходятся у 50 адресов), чтобы поздние результаты не скрывались.
## 10. Порядок по правилам проекта
План (этот файл) → реализация по шагам из п.7 → `…-summary.md` на каждый шаг → обновление `README.md` и `docs/` → обновление графа.
- Реализуется страница, точно повторяющая макет `docs/mockups/analytics-mockup.html`; отличия только там, где макет был заглушкой (данные, ссылки) или где этого требует рабочее приложение: время в подписях запусков — локальное время дашборда (как везде в нём), матрица «подсеть × цель» строится для любого типа проверки (в макете для `icmp` её не было), надпись про перепроверки показывается, только если они были.
- Решения по открытым вопросам плана приняты по умолчанию: перепроверка после завершения запуска открывает новый запуск; список подсетей хранится в БД (`subnets`) и задаётся на `/settings`, пока пуст — группы по /24.
- Миграция `0011` (запуски, `run_results`, `subnets`, `run_id`у`ip_queue` и `checks`, заполнение накопленных данных, валидатор у ingress-проверок). Раньше плана «Аналитика» выполнена миграция `0010` (вердикт без опоздавших результатов).
- Ссылки из подсетей ведут в «Реестр» с фильтрами `run` и `subnet` (добавлены в `GET /admin/registry` и `/registry`).
- **Сайдбар** (новое требование): убраны три точки возле логотипа; индикатор связи с control-api, переключатель темы и кнопка выхода подняты наверх в блок сессии под логотипом; индикатор краснеет («нет связи»), когда control-api недоступен; разделы разбиты на группы «Мониторинг» (Обзор, Очередь IP, Реестр, Аналитика) и «Настройка» (Валидаторы, Площадки, Цели, Типы проверок, Настройки); нижний блок сайдбара убран.