# План: сравнение двух запусков в разделе «Аналитика» Статус: реализовано, см. [итог](2026-10-04_10-08_analytics-run-compare-summary.md). ## 1. Что нужно Администратор выбирает два завершённых запуска и видит динамику между ними по семи индикаторам страницы «Аналитика»: `pass`, `partial`, `fail`, «Egress https: есть провалы», «Egress https: все провалены», «Ingress ssh: есть провалы», «Ingress ssh: все провалены». Отдельно выделяются три группы адресов: 1. **Новые**: есть в новом запуске, в старом не было. Нужны, чтобы понять, какие адреса прибыли в проект и подключились к анализу. 2. **Изменившиеся**: есть в обоих запусках, но состояние по проверкам разное. Для них показывается подробная сводка, что именно изменилось. 3. **Выбывшие**: были в старом запуске, в новом их нет. Нужны, чтобы понять, какие адреса вышли из состава проекта. Адреса, которые есть в обоих запусках и не изменились, только считаются (список доступен, но не выделяется). ## 2. Основные решения - **Отдельная страница `/analytics/compare`**, а не режим текущей: страница одного запуска остаётся как есть («данные других запусков на странице не участвуют»). На `/analytics` добавляется кнопка «Сравнить с другим запуском», она ведёт на сравнение с текущим запуском в роли нового. - **Выбор запусков.** Два списка: «Запуск A (старый)» и «Запуск B (новый)» и кнопка «Поменять местами». По умолчанию B — последний завершённый запуск, A — предыдущий. Порядок не навязывается: «новые» всегда означает «есть в B, нет в A». Доступны только завершённые запуски с адресами (как в текущем выборе). Одинаковые A и B — ошибка `400`. - **Адрес — это IP.** В запуске он присутствует, если у него есть итог, отличный от `cancelled`. Отменённые адреса из сравнения исключаются (в обоих запусках), их число показывается примечанием, чтобы они не выглядели как «новые» или «выбывшие». - **Состояние адреса** — принадлежность к семи индикаторам по последнему циклу адреса в запуске (те же правила, что в отчёте запуска; один адрес может входить в несколько, вердикт — ровно в один из трёх). Адрес **изменился**, если принадлежность хотя бы к одному индикатору в A и B разная. Адрес с теми же индикаторами, но другим набором проваленных целей или площадок считается неизменившимся (см. риски). - **Расчёт на control-api** по двум уже существующим `Analysis` (кэш завершённых запусков переиспользуется). Новых таблиц, миграций и новых данных нет. ## 3. Решение ### 3.1. control-api (`internal/analytics/compare.go`, новый) - Таблица семи индикаторов (ключ, название, предикат по адресу): ключи совпадают с видами списков `verdict_pass`, `verdict_partial`, `verdict_fail`, `egress_https_any`, `egress_https_all`, `ingress_ssh_any`, `ingress_ssh_all`. Предикаты повторяют условия `Compute`/`List`; тест проверяет, что число адресов по каждому индикатору в каждом запуске равно соответствующему полю `summary`. - `Compare(a, b *Analysis) *Comparison`: - группы: `new`, `left`, `common`, `changed`, `same` (числа; `common = changed + same`); - по каждому индикатору: `base`, `target`, `delta`, `new` (новые адреса в индикаторе), `left` (выбывшие, были в индикаторе), `entered` (общие адреса, вошедшие в индикатор), `exited` (общие адреса, вышедшие из индикатора). Инвариант: `delta = new − left + entered − exited` (проверяется тестом); - матрица переходов вердикта для общих адресов (3×3), плюс строка «нет в A» (новые) и столбец «нет в B» (выбывшие); - число отменённых адресов в каждом запуске. - `(*Comparison) List(group, filter)`: таблица адресов. Группы: `new`, `left`, `changed`, `same`, `entered`, `exited`. Фильтры: `indicator` (для `new`/`left` — адрес входит в индикатор в своём запуске; для `changed`/`same` — принадлежность этому индикатору; для `entered`/`exited` обязателен), `from`+`to` (вердикт в A и в B, для ячейки матрицы). Порядок — по числовому адресу. Неверная группа или индикатор — ошибка (`404`). - Столбцы списков: - `new`: Адрес, Подсеть, Вердикт, Egress, Ingress, Индикаторы (в B); - `left`: те же столбцы по запуску A; - `changed`, `entered`, `exited`, `same`: Адрес, Подсеть, Вердикт (A → B), Egress (A → B), Ingress (A → B), **Что изменилось**. «Что изменилось» — текст по шагам: «вердикт partial → pass», «вошёл в: Egress https: все провалены», «вышел из: Ingress ssh: есть провалы», «https: провалены цели +a.test −b.test», «ssh: площадки −rxmsk», «валидатор v3 → v12». У `same` — «без изменений». - Для текста нужны из `addr` уже имеющиеся поля (`https.failedTargets`, `ssh.sites`, `https.validator`, статистики egress/ingress); новых вычислений в `Compute` нет. ### 3.2. API (`internal/httpapi/handlers_analytics.go`) - `GET /api/v1/admin/analytics/compare?base=A&target=B` → отчёт сравнения (`runs`: сведения об обоих запусках, `groups`, `indicators`, `transitions`, `cancelled`). - `GET /api/v1/admin/analytics/compare/lists/{group}?base=A&target=B[&indicator=…][&from=…&to=…][&format=csv]` → таблица или CSV (UTF-8 с BOM; имя `compare_[_]_run-.csv`). - Ошибки: `400` (нет или неверные id, `base = target`), `404` (запуска нет, неизвестная группа/индикатор), `409` (запуск ещё идёт). - Загрузка `Analysis` по id выносится из `analysisFor` в общую функцию, чтобы сравнение и страница одного запуска пользовались одним кэшем. ### 3.3. Дашборд - Страница `/analytics/compare?base=A&target=B` (`handlers_analytics.go`, `templates/analytics_compare.html`, `static/analytics-compare.js`), прокси списков и CSV: `/analytics/compare/lists/{group}` и `/analytics/compare/csv/{group}`. Состояние страницы целиком в адресе (можно отправить ссылку). - Содержимое страницы сверху вниз: 1. Выбор A и B, «Поменять местами», «Сравнить»; под ним примечание: даты, тип и размер обоих запусков, число отменённых. 2. Карточки: «Новые», «Выбывшие», «Общие», «Изменились», «Без изменений». Кликабельны, открывают диалог со списком. 3. «Динамика по индикаторам»: строка на индикатор, столбцы A, B, Δ (рост `pass` и падение остальных — зелёным, обратное — красным), Новые, Выбывшие, Вошли, Вышли. Ненулевые числа кликабельны (список с соответствующими `group` и `indicator`). 4. «Переходы вердикта»: матрица 3×3 по общим адресам + «нет в A» / «нет в B»; ячейки кликабельны. 5. Диалог со списком, «Скачать CSV», «Копировать» — тот же, что на странице запуска. - Код диалога (открытие, заполнение, CSV, копирование, подсказки) выносится из `analytics.js` в общий `analytics-dialog.js`, разметка диалога — в общий шаблон; страница одного запуска переходит на него без изменения поведения. - На `/analytics` добавляется кнопка «Сравнить с другим запуском». ## 4. Файлы - `internal/analytics/compare.go` (новый), `internal/analytics/compare_test.go` (новый). - `internal/httpapi/handlers_analytics.go` (общий загрузчик, два новых обработчика), `routes`. - `internal/dashboard`: `handlers_analytics.go` (страница и прокси), `client.go`, `routes.go`, `templates/analytics_compare.html`, `templates/analytics.html` (кнопка, общий диалог), `static/analytics.js`, `static/analytics-dialog.js` (новый), `static/analytics-compare.js` (новый), `static/analytics.css`. - Документы: `docs/API.md`, `docs/USAGE.md` («Аналитика запусков»), `docs/DASHBOARD.md`, `README.md`, итог `docs/changes/…-summary.md`. ## 5. Тесты - `analytics`: два запуска с новыми, выбывшими, изменившимися и неизменившимися адресами, с отменёнными; числа групп; инвариант `delta = new − left + entered − exited` по каждому индикатору; число адресов по индикатору равно `summary` каждого запуска; матрица переходов; списки каждой группы и фильтры (`indicator`, `from`/`to`); текст «Что изменилось» (вердикт, вход/выход из индикатора, цели, площадки, валидатор); порядок по адресу; пустые группы. - `httpapi`: отчёт и списки (JSON, CSV с именем файла), `400` на одинаковые и неверные id, `404` на неизвестные запуск/группу/индикатор, `409` на незавершённый запуск. - `dashboard`: страница с запуском по умолчанию, с явным `base`/`target`, предупреждение на неверные id, прокси списка и CSV; страница одного запуска после выноса диалога по-прежнему отдаёт данные и скрипты. - `gofmt -l`, `go build ./... && go vet ./... && go test ./...`, проверка синтаксиса JS. ## 6. Выкладка и проверка Меняются `control-api` и `admin-dashboard` (статика встроена); агенты, prober, миграции без изменений. Порядок: проверка пустой очереди, тег отката образов, пересборка, перезапуск. Проверка на стенде: сравнение запусков 1 и 2 через API (сумма групп равна числу адресов, инвариант по индикаторам), затем страница в браузере: выбор запусков, обмен местами, открытие диалогов из карточек, таблицы и матрицы, CSV, а также что страница одного запуска работает как раньше. ## 7. Риски - **Что считается изменением.** Только принадлежность к семи индикаторам. Адрес, который остался в «Egress https: есть провалы», но у которого поменялись проваленные цели, попадает в «без изменений»; в списке `same` его сводка «без изменений» это не показывает. Если такие случаи важны, следующим шагом можно считать изменением и смену наборов целей и площадок. - **Размер списков.** Группа новых или `same` может содержать тысячи строк (диалог показывает все, как и другие списки); CSV полный. - **Разный состав запусков.** Если запуск 1 был частичным (например, перепроверка), то в сравнении будет много «новых» и «выбывших»: это отражает состав запусков, а не динамику. Размеры обоих запусков показаны в примечании над таблицами. - Вердикт — оценка системы при агрегации, а не итог всех проверок (см. «Качество данных»); сравнение вердикта это не меняет, столбцы Egress и Ingress показывают фактические проверки. - Вынос кода диалога затрагивает работающую страницу; закрывается тестами и ручной проверкой п. 6. ## 8. Не входит в доработку Сравнение более чем двух запусков, графики динамики по времени, разбивка новых и выбывших по подсетям, сравнение по другим показателям (классы ошибок, площадки, валидаторы), автоматический выбор «интересных» пар запусков. Открытых вопросов нет. Жду команды начать реализацию.